@page-speed/img 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2025, OpenSite AI. All rights reserved.
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md ADDED
@@ -0,0 +1,19 @@
1
+ ![Page Speed React Img](https://github.com/user-attachments/assets/dadfdfb0-fe28-4e9c-9576-5c5213616d82)
2
+
3
+ ---
4
+
5
+ # ⚡ @page-speed/img
6
+
7
+ **Performance-optimized React Image component**
8
+
9
+ Drop-in Image implementation of [web.dev](https://web.dev) best practices with zero configuration.
10
+
11
+ [![npm version](https://img.shields.io/npm/v/@page-speed/img?style=flat-square)](https://www.npmjs.com/package/@page-speed/hooks)
12
+ [![npm downloads](https://img.shields.io/npm/dm/@page-speed/img?style=flat-square)](https://www.npmjs.com/package/@page-speed/hooks)
13
+ [![License](https://img.shields.io/npm/l/@page-speed/img?style=flat-square)](./LICENSE)
14
+ [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue?style=flat-square)](./tsconfig.json)
15
+ [![Tree-Shakeable](https://img.shields.io/badge/Tree%20Shakeable-Yes-brightgreen?style=flat-square)](#tree-shaking)
16
+
17
+ [Documentation](#documentation) · [Quick Start](#quick-start) · [Hooks](#hooks) · [Examples](#examples) · [Contributing](./CONTRIBUTING.md)
18
+
19
+ ---
@@ -0,0 +1,2 @@
1
+ !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("react")):"function"==typeof define&&define.amd?define(["exports","react"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).OpensiteImg={},t.React)}(this,function(t,e){"use strict";const n=new Map;function i(t,e){n.set(t,e)}const r="https://cdn.ing";function l(t){return(t??r).replace(/\/$/,"")}function o(t,e){return`${l(e)}/assets/images/${t}`}function s(t,e){return`${l(e)}/i/r/${t}`}function a(t){return!!t&&["AVIF","WEBP","JPEG"].some(e=>{const n=null==t?void 0:t[e];return!!(i=n)&&[i.sm,i.md,i.lg,i.full].some(t=>"string"==typeof t&&t.trim().length>0);var i})}function u(t){return"string"==typeof t&&t.trim().length>0}function d(t){var e;if(!t)return!1;if(a((null==(e=t.variants_data)?void 0:e.variants)??null))return!0;const n=t;return[n.img_url,n.file_data_url,n.file_data_thumbnail_url,n.img_src,n.med_src,n.thumb_src,n.low_res_thumb].some(u)}async function c(t,e){const n=await fetch(t,{signal:e});if(!n.ok){const e=new Error(`Failed to fetch image data (status ${n.status}) from ${t}`);throw e.status=n.status,e}return await n.json()}async function f(t,e={}){if(!Number.isFinite(t))throw new Error("Invalid mediaId provided to fetchImageData");const r=l(e.cdnHost),a=`image:${r}:${t}`;if(!e.bypassCache){const t=(u=a,n.get(u));if(t)return t}var u;const f=[o(t,r),s(t,r)];let m;for(const n of f)try{const t=await c(n,e.signal);return d(t)&&i(a,t),t}catch(v){if("AbortError"===(null==v?void 0:v.name))throw v;m=v}if(m instanceof Error)throw m;throw new Error(`Failed to fetch image data for mediaId ${t}`)}const m="dt:media-selected";function v(t){t&&t.querySelectorAll("source").forEach(t=>{const e=t.getAttribute("srcset");e&&(t.setAttribute("data-srcset",e),t.removeAttribute("srcset"),requestAnimationFrame(()=>{t.setAttribute("srcset",e)}))})}const g={sm:640,md:1024,lg:1536,full:2560},h=t=>"string"==typeof t&&t.trim().length>0;function p(t){const e=null==t?void 0:t.widths;return e?{sm:e.small??e.sm??g.sm,md:e.medium??e.md??g.md,lg:e.large??e.lg??g.lg,full:e.full_size??e.full??g.full}:null}function w(t){if(t)return t.md||t.lg||t.sm||t.full||Object.values(t).find(Boolean)}const _=e.forwardRef(function({mediaId:t,cdnHost:n,sizes:i,onImageData:o,loading:s,decoding:u,alt:d,title:c,src:_,...E},b){const y=e.useRef(null);e.useImperativeHandle(b,()=>y.current);!function(t){e.useEffect(()=>{const e=t.current;e&&(e instanceof HTMLPictureElement?v(e):e.parentElement instanceof HTMLPictureElement&&v(e.parentElement))},[t])}(e.useRef(null)),e.useEffect(()=>{if("undefined"==typeof window)return;const t=()=>{};return window.addEventListener(m,t),()=>window.removeEventListener(m,t)},[]);const[$,I]=e.useState(null),[S,M]=e.useState(0),x=Number.isFinite(t),z=s??"lazy",A=u??"async",[O,N]=e.useState(()=>!x||"lazy"!==z),P=e.useMemo(()=>(n??r).replace(/\/$/,""),[n]);e.useEffect(()=>{if(!x)return I(null),void M(0);I(null),M(0)},[x,t,n]),e.useEffect(()=>{if(!x)return;const e=new AbortController;return f(t,{cdnHost:n,signal:e.signal,bypassCache:S>0}).then(t=>{I(t),null==o||o(t)}).catch(t=>{"AbortError"!==(null==t?void 0:t.name)&&console.warn("Image data fetch failed:",t)}),()=>e.abort()},[x,t,n,o,S]),e.useEffect(()=>{N(!x||"lazy"!==z)},[x,t,z]),e.useEffect(()=>{if(!x||"lazy"!==z||O)return;if("undefined"==typeof window||void 0===window.IntersectionObserver)return void N(!0);const t=y.current;if(!t)return;const e=new IntersectionObserver(t=>{t.some(t=>t.isIntersecting)&&(N(!0),e.disconnect())},{rootMargin:"200px"});return e.observe(t),()=>e.disconnect()},[x,z,O]);const T=e.useMemo(()=>{var t,e,n;if(!$)return null;const i=(null==(t=$.variants_data)?void 0:t.variants)??{},r=i.WEBP,l=i.AVIF,o=i.JPEG,s=p(null==(e=i.WEBP)?void 0:e.metadata)||p(null==(n=i.JPEG)?void 0:n.metadata)||{...g},a=t=>(t=>{if(h(t))return/^https?:\/\//i.test(t)||t.startsWith("data:")?t:t.startsWith("//")?`https:${t}`:t.startsWith("/")?`${P}${t}`:`${P}/${t}`})("string"==typeof t?t:void 0),u=[w(r),w(o),w(l),null==r?void 0:r.sm,null==r?void 0:r.md,null==r?void 0:r.lg,null==r?void 0:r.full,null==o?void 0:o.sm,null==o?void 0:o.md,null==o?void 0:o.lg,null==o?void 0:o.full,null==l?void 0:l.sm,null==l?void 0:l.md,null==l?void 0:l.lg,null==l?void 0:l.full].map(t=>a(t??void 0)).filter(h),d=$,c=[d.img_url,d.file_data_url,d.file_data_thumbnail_url,d.img_src,d.med_src,d.thumb_src,d.low_res_thumb].map(t=>h(t)?a(t):void 0).filter(h),f=d.fallback_url?[a(d.fallback_url)].filter(h):[],m=[...u,...c,...f][0];if(!m)return null;return{webp:r,avif:l,jpeg:o,toSrcSet:t=>{if(!t)return;const e=[],n=(t,n)=>{const i=a(t);i&&n&&e.push(`${i} ${n}w`)};return n(t.sm,s.sm),n(t.md,s.md),n(t.lg,s.lg),n(t.full,s.full),e.length?e.join(", "):void 0},fallback:m,widths:s,hasVariantSource:u.length>0}},[$,P]),j=e.useMemo(()=>{var t;return a((null==(t=null==$?void 0:$.variants_data)?void 0:t.variants)??null)},[$]),F=e.useMemo(()=>{var t;const e=(null==(t=null==$?void 0:$.variants_data)?void 0:t.status)??(null==$?void 0:$.variants_status)??"";return"string"==typeof e?e.toLowerCase():""},[$]),k="failed"===F||"error"===F,H=x&&Boolean($)&&!k&&!j&&S<5;e.useEffect(()=>{if(!H)return;if("undefined"==typeof window)return;const t=window.setTimeout(()=>{M(t=>t+1)},3e3);return()=>window.clearTimeout(t)},[H]);const L=e.useMemo(()=>{var t,e;return"string"==typeof d?d:(null==(e=null==(t=null==$?void 0:$.meta)?void 0:t.content_manifest)?void 0:e.summary)??void 0},[d,$]),V=e.useMemo(()=>{var t,e;return"string"==typeof c?c:(null==(e=null==(t=null==$?void 0:$.meta)?void 0:t.content_manifest)?void 0:e.title)??void 0},[c,$]),W=e.useMemo(()=>{var t,e,n,i;return(null==(e=null==(t=null==$?void 0:$.meta)?void 0:t.sizing)?void 0:e.width)??(null==(i=null==(n=null==$?void 0:$.variants_data)?void 0:n.metadata)?void 0:i.width)??void 0},[$]),B=e.useMemo(()=>{var t,e,n,i;return(null==(e=null==(t=null==$?void 0:$.meta)?void 0:t.sizing)?void 0:e.height)??(null==(i=null==(n=null==$?void 0:$.variants_data)?void 0:n.metadata)?void 0:i.height)??void 0},[$]),C=e.useMemo(()=>{var t,e;const n=null==(e=null==(t=null==$?void 0:$.meta)?void 0:t.content_manifest)?void 0:e.optimized_filename;if(!n)return;const i=null==T?void 0:T.fallback;if(!i)return;const r=i.lastIndexOf(".");return`${n}.${r>-1?i.slice(r+1).toLowerCase():"jpg"}`},[$,T]);if(!x){const t={...E};return e.createElement("img",{ref:y,src:_,loading:z,decoding:A,alt:L,title:V,width:t.width,height:t.height,...t})}const D=function(t,e){return`${l(e)}/assets/low_res_thumb/${t}`}(t,n);if(!$||!T||!O){const t={...E};return e.createElement("img",{ref:y,src:D,loading:z,decoding:A,alt:L,title:V,width:t.width??W,height:t.height??B,...t})}const R=i??"(max-width:640px) 640px, (max-width:1024px) 1024px, 1536px",{webp:q,avif:G,jpeg:J,toSrcSet:K,fallback:Q}=T,U=K(q),X=K(G),Y=K(J);return U||X||Y?e.createElement("picture",null,X?e.createElement("source",{type:"image/avif",srcSet:X,sizes:R}):null,U?e.createElement("source",{type:"image/webp",srcSet:U,sizes:R}):null,e.createElement("img",{ref:y,src:Q,srcSet:!Y||U||X?void 0:Y,sizes:!Y||U||X?void 0:R,loading:z,decoding:A,alt:L,title:V,width:W,height:B,"data-filename":C,...E})):e.createElement("img",{ref:y,src:Q,loading:z,decoding:A,alt:L,title:V,width:W,height:B,"data-filename":C,...E})}),E=e.memo(_);E.displayName="OpenSiteImg";const b="undefined"!=typeof globalThis?globalThis:void 0;if(b)if(b.process){const t=b.process.env??(b.process.env={});void 0===t.NODE_ENV&&(t.NODE_ENV="production")}else b.process={env:{NODE_ENV:"production"}};t.Img=E,Object.defineProperty(t,Symbol.toStringTag,{value:"Module"})});
2
+ //# sourceMappingURL=opensite-img.umd.js.map
@@ -0,0 +1,2 @@
1
+ !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("react")):"function"==typeof define&&define.amd?define(["exports","react"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).OpensiteImg={},t.React)}(this,function(t,e){"use strict";const n=new Map;function i(t,e){n.set(t,e)}const r="https://cdn.ing";function l(t){return(t??r).replace(/\/$/,"")}function o(t,e){return`${l(e)}/assets/images/${t}`}function s(t,e){return`${l(e)}/i/r/${t}`}function a(t){return!!t&&["AVIF","WEBP","JPEG"].some(e=>{const n=null==t?void 0:t[e];return!!(i=n)&&[i.sm,i.md,i.lg,i.full].some(t=>"string"==typeof t&&t.trim().length>0);var i})}function u(t){return"string"==typeof t&&t.trim().length>0}function d(t){var e;if(!t)return!1;if(a((null==(e=t.variants_data)?void 0:e.variants)??null))return!0;const n=t;return[n.img_url,n.file_data_url,n.file_data_thumbnail_url,n.img_src,n.med_src,n.thumb_src,n.low_res_thumb].some(u)}async function c(t,e){const n=await fetch(t,{signal:e});if(!n.ok){const e=new Error(`Failed to fetch image data (status ${n.status}) from ${t}`);throw e.status=n.status,e}return await n.json()}async function f(t,e={}){if(!Number.isFinite(t))throw new Error("Invalid mediaId provided to fetchImageData");const r=l(e.cdnHost),a=`image:${r}:${t}`;if(!e.bypassCache){const t=(u=a,n.get(u));if(t)return t}var u;const f=[o(t,r),s(t,r)];let m;for(const n of f)try{const t=await c(n,e.signal);return d(t)&&i(a,t),t}catch(v){if("AbortError"===(null==v?void 0:v.name))throw v;m=v}if(m instanceof Error)throw m;throw new Error(`Failed to fetch image data for mediaId ${t}`)}const m="dt:media-selected";function v(t){t&&t.querySelectorAll("source").forEach(t=>{const e=t.getAttribute("srcset");e&&(t.setAttribute("data-srcset",e),t.removeAttribute("srcset"),requestAnimationFrame(()=>{t.setAttribute("srcset",e)}))})}const g={sm:640,md:1024,lg:1536,full:2560},h=t=>"string"==typeof t&&t.trim().length>0;function p(t){const e=null==t?void 0:t.widths;return e?{sm:e.small??e.sm??g.sm,md:e.medium??e.md??g.md,lg:e.large??e.lg??g.lg,full:e.full_size??e.full??g.full}:null}function w(t){if(t)return t.md||t.lg||t.sm||t.full||Object.values(t).find(Boolean)}const _=e.forwardRef(function({mediaId:t,cdnHost:n,sizes:i,onImageData:o,loading:s,decoding:u,alt:d,title:c,src:_,...E},b){const y=e.useRef(null);e.useImperativeHandle(b,()=>y.current);!function(t){e.useEffect(()=>{const e=t.current;e&&(e instanceof HTMLPictureElement?v(e):e.parentElement instanceof HTMLPictureElement&&v(e.parentElement))},[t])}(e.useRef(null)),e.useEffect(()=>{if("undefined"==typeof window)return;const t=()=>{};return window.addEventListener(m,t),()=>window.removeEventListener(m,t)},[]);const[$,I]=e.useState(null),[S,M]=e.useState(0),x=Number.isFinite(t),z=s??"lazy",A=u??"async",[O,N]=e.useState(()=>!x||"lazy"!==z),P=e.useMemo(()=>(n??r).replace(/\/$/,""),[n]);e.useEffect(()=>{if(!x)return I(null),void M(0);I(null),M(0)},[x,t,n]),e.useEffect(()=>{if(!x)return;const e=new AbortController;return f(t,{cdnHost:n,signal:e.signal,bypassCache:S>0}).then(t=>{I(t),null==o||o(t)}).catch(t=>{"AbortError"!==(null==t?void 0:t.name)&&console.warn("Image data fetch failed:",t)}),()=>e.abort()},[x,t,n,o,S]),e.useEffect(()=>{N(!x||"lazy"!==z)},[x,t,z]),e.useEffect(()=>{if(!x||"lazy"!==z||O)return;if("undefined"==typeof window||void 0===window.IntersectionObserver)return void N(!0);const t=y.current;if(!t)return;const e=new IntersectionObserver(t=>{t.some(t=>t.isIntersecting)&&(N(!0),e.disconnect())},{rootMargin:"200px"});return e.observe(t),()=>e.disconnect()},[x,z,O]);const T=e.useMemo(()=>{var t,e,n;if(!$)return null;const i=(null==(t=$.variants_data)?void 0:t.variants)??{},r=i.WEBP,l=i.AVIF,o=i.JPEG,s=p(null==(e=i.WEBP)?void 0:e.metadata)||p(null==(n=i.JPEG)?void 0:n.metadata)||{...g},a=t=>(t=>{if(h(t))return/^https?:\/\//i.test(t)||t.startsWith("data:")?t:t.startsWith("//")?`https:${t}`:t.startsWith("/")?`${P}${t}`:`${P}/${t}`})("string"==typeof t?t:void 0),u=[w(r),w(o),w(l),null==r?void 0:r.sm,null==r?void 0:r.md,null==r?void 0:r.lg,null==r?void 0:r.full,null==o?void 0:o.sm,null==o?void 0:o.md,null==o?void 0:o.lg,null==o?void 0:o.full,null==l?void 0:l.sm,null==l?void 0:l.md,null==l?void 0:l.lg,null==l?void 0:l.full].map(t=>a(t??void 0)).filter(h),d=$,c=[d.img_url,d.file_data_url,d.file_data_thumbnail_url,d.img_src,d.med_src,d.thumb_src,d.low_res_thumb].map(t=>h(t)?a(t):void 0).filter(h),f=d.fallback_url?[a(d.fallback_url)].filter(h):[],m=[...u,...c,...f][0];if(!m)return null;return{webp:r,avif:l,jpeg:o,toSrcSet:t=>{if(!t)return;const e=[],n=(t,n)=>{const i=a(t);i&&n&&e.push(`${i} ${n}w`)};return n(t.sm,s.sm),n(t.md,s.md),n(t.lg,s.lg),n(t.full,s.full),e.length?e.join(", "):void 0},fallback:m,widths:s,hasVariantSource:u.length>0}},[$,P]),j=e.useMemo(()=>{var t;return a((null==(t=null==$?void 0:$.variants_data)?void 0:t.variants)??null)},[$]),F=e.useMemo(()=>{var t;const e=(null==(t=null==$?void 0:$.variants_data)?void 0:t.status)??(null==$?void 0:$.variants_status)??"";return"string"==typeof e?e.toLowerCase():""},[$]),k="failed"===F||"error"===F,H=x&&Boolean($)&&!k&&!j&&S<5;e.useEffect(()=>{if(!H)return;if("undefined"==typeof window)return;const t=window.setTimeout(()=>{M(t=>t+1)},3e3);return()=>window.clearTimeout(t)},[H]);const L=e.useMemo(()=>{var t,e;return"string"==typeof d?d:(null==(e=null==(t=null==$?void 0:$.meta)?void 0:t.content_manifest)?void 0:e.summary)??void 0},[d,$]),V=e.useMemo(()=>{var t,e;return"string"==typeof c?c:(null==(e=null==(t=null==$?void 0:$.meta)?void 0:t.content_manifest)?void 0:e.title)??void 0},[c,$]),W=e.useMemo(()=>{var t,e,n,i;return(null==(e=null==(t=null==$?void 0:$.meta)?void 0:t.sizing)?void 0:e.width)??(null==(i=null==(n=null==$?void 0:$.variants_data)?void 0:n.metadata)?void 0:i.width)??void 0},[$]),B=e.useMemo(()=>{var t,e,n,i;return(null==(e=null==(t=null==$?void 0:$.meta)?void 0:t.sizing)?void 0:e.height)??(null==(i=null==(n=null==$?void 0:$.variants_data)?void 0:n.metadata)?void 0:i.height)??void 0},[$]),C=e.useMemo(()=>{var t,e;const n=null==(e=null==(t=null==$?void 0:$.meta)?void 0:t.content_manifest)?void 0:e.optimized_filename;if(!n)return;const i=null==T?void 0:T.fallback;if(!i)return;const r=i.lastIndexOf(".");return`${n}.${r>-1?i.slice(r+1).toLowerCase():"jpg"}`},[$,T]);if(!x){const t={...E};return e.createElement("img",{ref:y,src:_,loading:z,decoding:A,alt:L,title:V,width:t.width,height:t.height,...t})}const D=function(t,e){return`${l(e)}/assets/low_res_thumb/${t}`}(t,n);if(!$||!T||!O){const t={...E};return e.createElement("img",{ref:y,src:D,loading:z,decoding:A,alt:L,title:V,width:t.width??W,height:t.height??B,...t})}const R=i??"(max-width:640px) 640px, (max-width:1024px) 1024px, 1536px",{webp:q,avif:G,jpeg:J,toSrcSet:K,fallback:Q}=T,U=K(q),X=K(G),Y=K(J);return U||X||Y?e.createElement("picture",null,X?e.createElement("source",{type:"image/avif",srcSet:X,sizes:R}):null,U?e.createElement("source",{type:"image/webp",srcSet:U,sizes:R}):null,e.createElement("img",{ref:y,src:Q,srcSet:!Y||U||X?void 0:Y,sizes:!Y||U||X?void 0:R,loading:z,decoding:A,alt:L,title:V,width:W,height:B,"data-filename":C,...E})):e.createElement("img",{ref:y,src:Q,loading:z,decoding:A,alt:L,title:V,width:W,height:B,"data-filename":C,...E})}),E=e.memo(_);E.displayName="OpenSiteImg";const b="undefined"!=typeof globalThis?globalThis:void 0;if(b)if(b.process){const t=b.process.env??(b.process.env={});void 0===t.NODE_ENV&&(t.NODE_ENV="production")}else b.process={env:{NODE_ENV:"production"}};t.Img=E,Object.defineProperty(t,Symbol.toStringTag,{value:"Module"})});
2
+ //# sourceMappingURL=opensite-img.umd.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opensite-img.umd.js","sources":["../../src/utils/cache.ts","../../src/utils/api.ts","../../src/core/useMediaSelectionEffect.ts","../../src/core/useResponsiveReset.ts","../../src/core/Img.tsx","../../src/index.ts"],"sourcesContent":["// Lightweight multi-level cache inspired by ecosystem guidelines\n// L1 (instance) will be handled in components via refs/state; this is L2 (module-level) cache\n\nconst l2Cache = new Map<string, any>();\n\nexport function cacheGet<T>(key: string): T | undefined {\n return l2Cache.get(key) as T | undefined;\n}\n\nexport function cacheSet<T>(key: string, value: T) {\n l2Cache.set(key, value);\n}\n\nexport function cacheHas(key: string) {\n return l2Cache.has(key);\n}\n\n","import { cacheGet, cacheSet } from './cache.js';\nimport type { ImageData, ImageVariantsMap, ProgressiveSizes } from '../types.js';\n\nexport type FetchImageOptions = {\n cdnHost?: string; // default: https://cdn.ing\n signal?: AbortSignal;\n bypassCache?: boolean;\n};\n\nexport const DEFAULT_CDN_HOST = 'https://cdn.ing';\n\nfunction normalizeHost(cdnHost?: string): string {\n return (cdnHost ?? DEFAULT_CDN_HOST).replace(/\\/$/, '');\n}\n\nfunction buildPrimaryImageUrl(mediaId: number, cdnHost?: string): string {\n const host = normalizeHost(cdnHost);\n return `${host}/assets/images/${mediaId}`;\n}\n\nfunction buildLegacyImageUrl(mediaId: number, cdnHost?: string): string {\n const host = normalizeHost(cdnHost);\n return `${host}/i/r/${mediaId}`;\n}\n\nexport function buildPlaceholderImageUrl(mediaId: number, cdnHost?: string): string {\n const host = normalizeHost(cdnHost);\n return `${host}/assets/low_res_thumb/${mediaId}`;\n}\n\nfunction hasRenderableUrlVariant(variant?: ProgressiveSizes | null): boolean {\n if (!variant) return false;\n const candidates = [variant.sm, variant.md, variant.lg, variant.full];\n return candidates.some((value) => typeof value === 'string' && value.trim().length > 0);\n}\n\nexport function imageVariantsHaveRenderableSource(variants?: ImageVariantsMap | null): boolean {\n if (!variants) return false;\n return ['AVIF', 'WEBP', 'JPEG'].some((format) => {\n const entry = variants?.[format as keyof ImageVariantsMap] as ProgressiveSizes | undefined;\n return hasRenderableUrlVariant(entry);\n });\n}\n\nfunction hasUrlValue(value: unknown): value is string {\n return typeof value === 'string' && value.trim().length > 0;\n}\n\nexport function imageDataHasRenderableSource(data: ImageData): boolean {\n if (!data) return false;\n if (imageVariantsHaveRenderableSource(data.variants_data?.variants ?? null)) {\n return true;\n }\n const raw = data as any;\n const directFields = [\n raw.img_url,\n raw.file_data_url,\n raw.file_data_thumbnail_url,\n raw.img_src,\n raw.med_src,\n raw.thumb_src,\n raw.low_res_thumb,\n ];\n return directFields.some(hasUrlValue);\n}\n\nasync function fetchFrom(url: string, signal?: AbortSignal): Promise<ImageData> {\n const res = await fetch(url, { signal });\n if (!res.ok) {\n const error = new Error(`Failed to fetch image data (status ${res.status}) from ${url}`);\n (error as any).status = res.status;\n throw error;\n }\n return (await res.json()) as ImageData;\n}\n\nexport async function fetchImageData(\n mediaId: number,\n options: FetchImageOptions = {}\n): Promise<ImageData> {\n if (!Number.isFinite(mediaId)) {\n throw new Error('Invalid mediaId provided to fetchImageData');\n }\n\n const host = normalizeHost(options.cdnHost);\n const cacheKey = `image:${host}:${mediaId}`;\n if (!options.bypassCache) {\n const cached = cacheGet<ImageData>(cacheKey);\n if (cached) return cached;\n }\n\n const urls = [buildPrimaryImageUrl(mediaId, host), buildLegacyImageUrl(mediaId, host)];\n let lastError: unknown;\n\n for (const url of urls) {\n try {\n const data = await fetchFrom(url, options.signal);\n if (imageDataHasRenderableSource(data)) {\n cacheSet(cacheKey, data);\n }\n return data;\n } catch (err) {\n if ((err as any)?.name === 'AbortError') {\n throw err;\n }\n lastError = err;\n }\n }\n\n if (lastError instanceof Error) {\n throw lastError;\n }\n throw new Error(`Failed to fetch image data for mediaId ${mediaId}`);\n}\n","import { useEffect } from 'react';\n\nconst MEDIA_SELECTED_EVENT = 'dt:media-selected';\n\nexport function sendMediaSelection(blockId: string, payload: unknown) {\n if (typeof window === 'undefined') return;\n window.dispatchEvent(\n new CustomEvent(MEDIA_SELECTED_EVENT, {\n detail: { blockId, payload },\n })\n );\n}\n\nexport function useMediaSelectionEffect() {\n useEffect(() => {\n if (typeof window === 'undefined') return;\n const handler = () => {\n // no-op: the real handler is attached in the builder via addEventListener\n };\n window.addEventListener(MEDIA_SELECTED_EVENT, handler);\n return () => window.removeEventListener(MEDIA_SELECTED_EVENT, handler);\n }, []);\n}\n\n","import { useEffect } from 'react';\n\nexport function resetResponsivePictureState(element: HTMLPictureElement | null) {\n if (!element) return;\n element.querySelectorAll('source').forEach((source) => {\n // force browser to reconsider responsive sources\n const srcset = source.getAttribute('srcset');\n if (srcset) {\n source.setAttribute('data-srcset', srcset);\n source.removeAttribute('srcset');\n requestAnimationFrame(() => {\n source.setAttribute('srcset', srcset);\n });\n }\n });\n}\n\nexport function useResponsiveReset(ref: React.RefObject<HTMLPictureElement | HTMLImageElement>) {\n useEffect(() => {\n const element = ref.current;\n if (!element) return;\n if (element instanceof HTMLPictureElement) {\n resetResponsivePictureState(element);\n } else if (element.parentElement instanceof HTMLPictureElement) {\n resetResponsivePictureState(element.parentElement);\n }\n }, [ref]);\n}\n\n","\"use client\";\n\nimport React, { forwardRef, memo, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';\nimport type { ImageData, ProgressiveSizes } from '../types.js';\nimport {\n DEFAULT_CDN_HOST,\n buildPlaceholderImageUrl,\n fetchImageData,\n imageVariantsHaveRenderableSource,\n} from '../utils/api.js';\nimport { useMediaSelectionEffect } from './useMediaSelectionEffect.js';\nimport { useResponsiveReset } from './useResponsiveReset.js';\n\ntype NativeImgProps = Omit<React.ImgHTMLAttributes<HTMLImageElement>, 'src' | 'srcSet' | 'sizes'> & {\n src?: string;\n};\n\nexport type ImgProps = NativeImgProps & {\n mediaId?: number;\n cdnHost?: string;\n sizes?: string;\n onImageData?: (data: ImageData) => void;\n};\n\nconst DEFAULT_WIDTHS = {\n sm: 640,\n md: 1024,\n lg: 1536,\n full: 2560,\n} as const;\n\nconst MAX_VARIANT_REFRESH_ATTEMPTS = 5;\nconst VARIANT_REFRESH_DELAY_MS = 3000;\n\nconst isUrlString = (value: unknown): value is string => typeof value === 'string' && value.trim().length > 0;\n\nfunction widthMapFromMetadata(v?: any): Record<'sm' | 'md' | 'lg' | 'full', number> | null {\n const w = v?.widths as any;\n if (!w) return null;\n return {\n sm: w.small ?? w.sm ?? DEFAULT_WIDTHS.sm,\n md: w.medium ?? w.md ?? DEFAULT_WIDTHS.md,\n lg: w.large ?? w.lg ?? DEFAULT_WIDTHS.lg,\n full: w.full_size ?? w.full ?? DEFAULT_WIDTHS.full,\n };\n}\n\nfunction pickBest(sizes: ProgressiveSizes | undefined): string | undefined {\n if (!sizes) return undefined;\n return sizes.md || sizes.lg || sizes.sm || sizes.full || Object.values(sizes).find(Boolean);\n}\n\nconst DEFAULT_SIZES = '(max-width:640px) 640px, (max-width:1024px) 1024px, 1536px';\n\nconst ImgBase = forwardRef<HTMLImageElement, ImgProps>(function Img(\n { mediaId, cdnHost, sizes, onImageData, loading, decoding, alt, title, src: directSrc, ...rest },\n ref,\n) {\n const imgRef = useRef<HTMLImageElement>(null);\n useImperativeHandle(ref, () => imgRef.current as HTMLImageElement);\n const pictureRef = useRef<HTMLPictureElement | null>(null);\n useResponsiveReset(pictureRef);\n useMediaSelectionEffect();\n\n const [data, setData] = useState<ImageData | null>(null);\n const [retryCount, setRetryCount] = useState(0);\n const hasMediaId = Number.isFinite(mediaId as number);\n const loadingAttr = loading ?? 'lazy';\n const decodingAttr = decoding ?? 'async';\n const [isInView, setIsInView] = useState(() => !hasMediaId || loadingAttr !== 'lazy');\n const cdnOrigin = useMemo(() => (cdnHost ?? DEFAULT_CDN_HOST).replace(/\\/$/, ''), [cdnHost]);\n\n useEffect(() => {\n if (!hasMediaId) {\n setData(null);\n setRetryCount(0);\n return;\n }\n setData(null);\n setRetryCount(0);\n }, [hasMediaId, mediaId, cdnHost]);\n\n useEffect(() => {\n if (!hasMediaId) {\n return;\n }\n const controller = new AbortController();\n fetchImageData(mediaId as number, {\n cdnHost,\n signal: controller.signal,\n bypassCache: retryCount > 0,\n })\n .then((d) => {\n setData(d);\n onImageData?.(d);\n })\n .catch((err) => {\n if (err?.name !== 'AbortError') {\n // eslint-disable-next-line no-console\n console.warn('Image data fetch failed:', err);\n }\n });\n return () => controller.abort();\n }, [hasMediaId, mediaId, cdnHost, onImageData, retryCount]);\n\n useEffect(() => {\n if (!hasMediaId || loadingAttr !== 'lazy') {\n setIsInView(true);\n return;\n }\n setIsInView(false);\n }, [hasMediaId, mediaId, loadingAttr]);\n\n useEffect(() => {\n if (!hasMediaId || loadingAttr !== 'lazy' || isInView) {\n return;\n }\n if (typeof window === 'undefined' || typeof window.IntersectionObserver === 'undefined') {\n setIsInView(true);\n return;\n }\n const node = imgRef.current;\n if (!node) {\n return;\n }\n const observer = new IntersectionObserver((entries) => {\n if (entries.some((entry) => entry.isIntersecting)) {\n setIsInView(true);\n observer.disconnect();\n }\n }, { rootMargin: '200px' });\n observer.observe(node);\n return () => observer.disconnect();\n }, [hasMediaId, loadingAttr, isInView]);\n\n // Build picture/source/srcset from variants\n const picture = useMemo(() => {\n if (!data) return null;\n const v = data.variants_data?.variants ?? {};\n const webp = (v as any).WEBP as ProgressiveSizes | undefined;\n const avif = (v as any).AVIF as ProgressiveSizes | undefined;\n const jpeg = (v as any).JPEG as ProgressiveSizes | undefined;\n\n const widths =\n widthMapFromMetadata((v as any).WEBP?.metadata) ||\n widthMapFromMetadata((v as any).JPEG?.metadata) ||\n { ...DEFAULT_WIDTHS };\n\n const ensureAbsolute = (url?: string) => {\n if (!isUrlString(url)) return undefined;\n if (/^https?:\\/\\//i.test(url) || url.startsWith('data:')) return url;\n if (url.startsWith('//')) return `https:${url}`;\n if (url.startsWith('/')) return `${cdnOrigin}${url}`;\n return `${cdnOrigin}/${url}`;\n };\n\n const normalizeCandidate = (candidate?: string | null) =>\n ensureAbsolute(typeof candidate === 'string' ? candidate : undefined);\n\n const variantCandidates = [\n pickBest(webp),\n pickBest(jpeg),\n pickBest(avif),\n webp?.sm,\n webp?.md,\n webp?.lg,\n webp?.full,\n jpeg?.sm,\n jpeg?.md,\n jpeg?.lg,\n jpeg?.full,\n avif?.sm,\n avif?.md,\n avif?.lg,\n avif?.full,\n ]\n .map((candidate) => normalizeCandidate(candidate ?? undefined))\n .filter(isUrlString);\n\n const raw = data as any;\n const directCandidates = [\n raw.img_url,\n raw.file_data_url,\n raw.file_data_thumbnail_url,\n raw.img_src,\n raw.med_src,\n raw.thumb_src,\n raw.low_res_thumb,\n ]\n .map((candidate) => (isUrlString(candidate) ? normalizeCandidate(candidate) : undefined))\n .filter(isUrlString);\n\n // Add fallback_url as the final option if no variants or direct candidates\n const fallbackCandidates = raw.fallback_url ? [normalizeCandidate(raw.fallback_url)].filter(isUrlString) : [];\n\n const fallback = [...variantCandidates, ...directCandidates, ...fallbackCandidates][0];\n\n if (!fallback) {\n return null;\n }\n\n const toSrcSet = (sizes?: ProgressiveSizes) => {\n if (!sizes) return undefined;\n const entries: string[] = [];\n const push = (url?: string, width?: number) => {\n const absolute = normalizeCandidate(url);\n if (absolute && width) entries.push(`${absolute} ${width}w`);\n };\n push(sizes.sm, widths.sm);\n push(sizes.md, widths.md);\n push(sizes.lg, widths.lg);\n push(sizes.full, widths.full);\n return entries.length ? entries.join(', ') : undefined;\n };\n\n return { webp, avif, jpeg, toSrcSet, fallback, widths, hasVariantSource: variantCandidates.length > 0 } as const;\n }, [data, cdnOrigin]);\n\n const hasVariantEntries = useMemo(\n () => imageVariantsHaveRenderableSource(data?.variants_data?.variants ?? null),\n [data],\n );\n\n const variantsStatus = useMemo(() => {\n const status = (data?.variants_data?.status ?? data?.variants_status) ?? '';\n return typeof status === 'string' ? status.toLowerCase() : '';\n }, [data]);\n\n const variantsFailed = variantsStatus === 'failed' || variantsStatus === 'error';\n\n const shouldPollForVariants =\n hasMediaId && Boolean(data) && !variantsFailed && !hasVariantEntries && retryCount < MAX_VARIANT_REFRESH_ATTEMPTS;\n\n useEffect(() => {\n if (!shouldPollForVariants) {\n return;\n }\n if (typeof window === 'undefined') {\n return;\n }\n const timeoutId = window.setTimeout(() => {\n setRetryCount((count) => count + 1);\n }, VARIANT_REFRESH_DELAY_MS);\n return () => window.clearTimeout(timeoutId);\n }, [shouldPollForVariants]);\n\n // Map HTML attributes from content manifest and sizing\n const altAttr = useMemo(() => {\n if (typeof alt === 'string') return alt;\n return (data?.meta as any)?.content_manifest?.summary ?? undefined;\n }, [alt, data]);\n\n const titleAttr = useMemo(() => {\n if (typeof title === 'string') return title;\n return (data?.meta as any)?.content_manifest?.title ?? undefined;\n }, [title, data]);\n\n const widthAttr = useMemo(() => data?.meta?.sizing?.width ?? data?.variants_data?.metadata?.width ?? undefined, [data]);\n const heightAttr = useMemo(() => data?.meta?.sizing?.height ?? data?.variants_data?.metadata?.height ?? undefined, [data]);\n\n // Compute data-filename for consumers that need semantic filenames\n const dataFilename = useMemo(() => {\n const base = (data?.meta as any)?.content_manifest?.optimized_filename as string | undefined;\n if (!base) return undefined;\n // ext derived from chosen fallback url\n const href = picture?.fallback;\n if (!href) return undefined;\n const dot = href.lastIndexOf('.');\n const ext = dot > -1 ? href.slice(dot + 1).toLowerCase() : 'jpg';\n return `${base}.${ext}`;\n }, [data, picture]);\n\n // If mediaId not provided but src is, render plain img\n if (!hasMediaId) {\n const r: any = { ...rest };\n return (\n <img\n ref={imgRef}\n src={directSrc}\n loading={loadingAttr}\n decoding={decodingAttr}\n alt={altAttr}\n title={titleAttr}\n width={r.width}\n height={r.height}\n {...r}\n />\n );\n }\n\n const placeholderSrc = buildPlaceholderImageUrl(mediaId as number, cdnHost);\n\n if (!data || !picture || !isInView) {\n const r: any = { ...rest };\n return (\n <img\n ref={imgRef}\n src={placeholderSrc}\n loading={loadingAttr}\n decoding={decodingAttr}\n alt={altAttr}\n title={titleAttr}\n width={r.width ?? widthAttr}\n height={r.height ?? heightAttr}\n {...r}\n />\n );\n }\n\n const sizesAttr = sizes ?? DEFAULT_SIZES;\n const { webp, avif, jpeg, toSrcSet, fallback } = picture;\n\n const webpSet = toSrcSet(webp);\n const avifSet = toSrcSet(avif);\n const jpegSet = toSrcSet(jpeg);\n\n if (webpSet || avifSet || jpegSet) {\n return (\n <picture>\n {avifSet ? <source type=\"image/avif\" srcSet={avifSet} sizes={sizesAttr} /> : null}\n {webpSet ? <source type=\"image/webp\" srcSet={webpSet} sizes={sizesAttr} /> : null}\n <img\n ref={imgRef}\n src={fallback}\n srcSet={jpegSet && !webpSet && !avifSet ? jpegSet : undefined}\n sizes={jpegSet && !webpSet && !avifSet ? sizesAttr : undefined}\n loading={loadingAttr}\n decoding={decodingAttr}\n alt={altAttr}\n title={titleAttr}\n width={widthAttr}\n height={heightAttr}\n data-filename={dataFilename}\n {...rest}\n />\n </picture>\n );\n }\n\n return (\n <img\n ref={imgRef}\n src={fallback}\n loading={loadingAttr}\n decoding={decodingAttr}\n alt={altAttr}\n title={titleAttr}\n width={widthAttr}\n height={heightAttr}\n data-filename={dataFilename}\n {...rest}\n />\n );\n});\n\nexport const Img = memo(ImgBase);\nImg.displayName = 'OpenSiteImg';\n","// Ensure process.env exists when the module is loaded directly in the browser UMD build.\ntype GlobalWithProcess = typeof globalThis & { process?: NodeJS.Process };\n\nconst globalObject =\n typeof globalThis !== 'undefined' ? (globalThis as GlobalWithProcess) : undefined;\n\nif (globalObject) {\n if (!globalObject.process) {\n globalObject.process = {\n env: { NODE_ENV: 'production' } as NodeJS.ProcessEnv,\n } as NodeJS.Process;\n } else {\n const env = globalObject.process.env ?? (globalObject.process.env = {} as NodeJS.ProcessEnv);\n if (typeof env.NODE_ENV === 'undefined') {\n env.NODE_ENV = 'production';\n }\n }\n}\n\nexport * from './core/index.js';\nexport * from './types.js';\n"],"names":["l2Cache","Map","cacheSet","key","value","set","DEFAULT_CDN_HOST","normalizeHost","cdnHost","replace","buildPrimaryImageUrl","mediaId","buildLegacyImageUrl","imageVariantsHaveRenderableSource","variants","some","format","entry","variant","sm","md","lg","full","trim","length","hasUrlValue","imageDataHasRenderableSource","data","_a","variants_data","raw","img_url","file_data_url","file_data_thumbnail_url","img_src","med_src","thumb_src","low_res_thumb","async","fetchFrom","url","signal","res","fetch","ok","error","Error","status","json","fetchImageData","options","Number","isFinite","host","cacheKey","bypassCache","cached","get","urls","lastError","err","name","MEDIA_SELECTED_EVENT","resetResponsivePictureState","element","querySelectorAll","forEach","source","srcset","getAttribute","setAttribute","removeAttribute","requestAnimationFrame","DEFAULT_WIDTHS","isUrlString","widthMapFromMetadata","v","w","widths","small","medium","large","full_size","pickBest","sizes","Object","values","find","Boolean","ImgBase","forwardRef","onImageData","loading","decoding","alt","title","src","directSrc","rest","ref","imgRef","useRef","useImperativeHandle","current","useEffect","HTMLPictureElement","parentElement","useResponsiveReset","window","handler","addEventListener","removeEventListener","setData","useState","retryCount","setRetryCount","hasMediaId","loadingAttr","decodingAttr","isInView","setIsInView","cdnOrigin","useMemo","controller","AbortController","then","d","catch","console","warn","abort","IntersectionObserver","node","observer","entries","isIntersecting","disconnect","rootMargin","observe","picture","webp","WEBP","avif","AVIF","jpeg","JPEG","_b","metadata","_c","normalizeCandidate","candidate","test","startsWith","ensureAbsolute","variantCandidates","map","filter","directCandidates","fallbackCandidates","fallback_url","fallback","toSrcSet","push","width","absolute","join","hasVariantSource","hasVariantEntries","variantsStatus","variants_status","toLowerCase","variantsFailed","shouldPollForVariants","timeoutId","setTimeout","count","clearTimeout","altAttr","meta","content_manifest","summary","titleAttr","widthAttr","sizing","_d","heightAttr","height","dataFilename","base","optimized_filename","href","dot","lastIndexOf","slice","r","React","createElement","placeholderSrc","buildPlaceholderImageUrl","sizesAttr","webpSet","avifSet","jpegSet","type","srcSet","Img","memo","displayName","globalObject","globalThis","process","env","NODE_ENV"],"mappings":"qRAGA,MAAMA,MAAcC,IAMb,SAASC,EAAYC,EAAaC,GACvCJ,EAAQK,IAAIF,EAAKC,EACnB,CCFO,MAAME,EAAmB,kBAEhC,SAASC,EAAcC,GACrB,OAAQA,GAAWF,GAAkBG,QAAQ,MAAO,GACtD,CAEA,SAASC,EAAqBC,EAAiBH,GAE7C,MAAO,GADMD,EAAcC,oBACKG,GAClC,CAEA,SAASC,EAAoBD,EAAiBH,GAE5C,MAAO,GADMD,EAAcC,UACLG,GACxB,CAaO,SAASE,EAAkCC,GAChD,QAAKA,GACE,CAAC,OAAQ,OAAQ,QAAQC,KAAMC,IACpC,MAAMC,EAAQ,MAAAH,OAAA,EAAAA,EAAWE,GACzB,SAV6BE,EAUED,IARd,CAACC,EAAQC,GAAID,EAAQE,GAAIF,EAAQG,GAAIH,EAAQI,MAC9CP,KAAMX,GAA2B,iBAAVA,GAAsBA,EAAMmB,OAAOC,OAAS,GAHvF,IAAiCN,GAYjC,CAEA,SAASO,EAAYrB,GACnB,MAAwB,iBAAVA,GAAsBA,EAAMmB,OAAOC,OAAS,CAC5D,CAEO,SAASE,EAA6BC,SAC3C,IAAKA,EAAM,OAAO,EAClB,GAAId,GAAkC,OAAAe,EAAAD,EAAKE,oBAAL,EAAAD,EAAoBd,WAAY,MACpE,OAAO,EAET,MAAMgB,EAAMH,EAUZ,MATqB,CACnBG,EAAIC,QACJD,EAAIE,cACJF,EAAIG,wBACJH,EAAII,QACJJ,EAAIK,QACJL,EAAIM,UACJN,EAAIO,eAEctB,KAAKU,EAC3B,CAEAa,eAAeC,EAAUC,EAAaC,GACpC,MAAMC,QAAYC,MAAMH,EAAK,CAAEC,WAC/B,IAAKC,EAAIE,GAAI,CACX,MAAMC,EAAQ,IAAIC,MAAM,sCAAsCJ,EAAIK,gBAAgBP,KAElF,MADCK,EAAcE,OAASL,EAAIK,OACtBF,CACR,CACA,aAAcH,EAAIM,MACpB,CAEAV,eAAsBW,EACpBtC,EACAuC,EAA6B,IAE7B,IAAKC,OAAOC,SAASzC,GACnB,MAAM,IAAImC,MAAM,8CAGlB,MAAMO,EAAO9C,EAAc2C,EAAQ1C,SAC7B8C,EAAW,SAASD,KAAQ1C,IAClC,IAAKuC,EAAQK,YAAa,CACxB,MAAMC,GDlFkBrD,ECkFWmD,EDjF9BtD,EAAQyD,IAAItD,ICkFjB,GAAIqD,EAAQ,OAAOA,CACrB,CDpFK,IAAqBrD,ECsF1B,MAAMuD,EAAO,CAAChD,EAAqBC,EAAS0C,GAAOzC,EAAoBD,EAAS0C,IAChF,IAAIM,EAEJ,IAAA,MAAWnB,KAAOkB,EAChB,IACE,MAAM/B,QAAaY,EAAUC,EAAKU,EAAQT,QAI1C,OAHIf,EAA6BC,IAC/BzB,EAASoD,EAAU3B,GAEdA,CACT,OAASiC,GACP,GAA2B,gBAAtB,MAAAA,OAAA,EAAAA,EAAaC,MAChB,MAAMD,EAERD,EAAYC,CACd,CAGF,GAAID,aAAqBb,MACvB,MAAMa,EAER,MAAM,IAAIb,MAAM,0CAA0CnC,IAC5D,CC/GA,MAAMmD,EAAuB,oBCAtB,SAASC,EAA4BC,GACrCA,GACLA,EAAQC,iBAAiB,UAAUC,QAASC,IAE1C,MAAMC,EAASD,EAAOE,aAAa,UAC/BD,IACFD,EAAOG,aAAa,cAAeF,GACnCD,EAAOI,gBAAgB,UACvBC,sBAAsB,KACpBL,EAAOG,aAAa,SAAUF,OAItC,CCSA,MAAMK,EAAiB,CACrBtD,GAAI,IACJC,GAAI,KACJC,GAAI,KACJC,KAAM,MAMFoD,EAAetE,GAAqD,iBAAVA,GAAsBA,EAAMmB,OAAOC,OAAS,EAE5G,SAASmD,EAAqBC,GAC5B,MAAMC,EAAI,MAAAD,OAAA,EAAAA,EAAGE,OACb,OAAKD,EACE,CACL1D,GAAI0D,EAAEE,OAASF,EAAE1D,IAAMsD,EAAetD,GACtCC,GAAIyD,EAAEG,QAAUH,EAAEzD,IAAMqD,EAAerD,GACvCC,GAAIwD,EAAEI,OAASJ,EAAExD,IAAMoD,EAAepD,GACtCC,KAAMuD,EAAEK,WAAaL,EAAEvD,MAAQmD,EAAenD,MALjC,IAOjB,CAEA,SAAS6D,EAASC,GAChB,GAAKA,EACL,OAAOA,EAAMhE,IAAMgE,EAAM/D,IAAM+D,EAAMjE,IAAMiE,EAAM9D,MAAQ+D,OAAOC,OAAOF,GAAOG,KAAKC,QACrF,CAEA,MAEMC,EAAUC,EAAAA,WAAuC,UACrD/E,QAAEA,UAASH,EAAA4E,MAASA,EAAAO,YAAOA,UAAaC,EAAAC,SAASA,EAAAC,IAAUA,QAAKC,EAAOC,IAAKC,KAAcC,GAC1FC,GAEA,MAAMC,EAASC,EAAAA,OAAyB,MACxCC,EAAAA,oBAAoBH,EAAK,IAAMC,EAAOG,UD1CjC,SAA4BJ,GACjCK,EAAAA,UAAU,KACR,MAAMxC,EAAUmC,EAAII,QACfvC,IACDA,aAAmByC,mBACrB1C,EAA4BC,GACnBA,EAAQ0C,yBAAyBD,oBAC1C1C,EAA4BC,EAAQ0C,iBAErC,CAACP,GACN,CCkCEQ,CADmBN,EAAAA,OAAkC,OF9CrDG,EAAAA,UAAU,KACR,GAAsB,oBAAXI,OAAwB,OACnC,MAAMC,EAAU,OAIhB,OADAD,OAAOE,iBAAiBhD,EAAsB+C,GACvC,IAAMD,OAAOG,oBAAoBjD,EAAsB+C,IAC7D,IE2CH,MAAOlF,EAAMqF,GAAWC,EAAAA,SAA2B,OAC5CC,EAAYC,GAAiBF,EAAAA,SAAS,GACvCG,EAAajE,OAAOC,SAASzC,GAC7B0G,EAAczB,GAAW,OACzB0B,EAAezB,GAAY,SAC1B0B,EAAUC,GAAeP,EAAAA,SAAS,KAAOG,GAA8B,SAAhBC,GACxDI,EAAYC,EAAAA,QAAQ,KAAOlH,GAAWF,GAAkBG,QAAQ,MAAO,IAAK,CAACD,IAEnFgG,EAAAA,UAAU,KACR,IAAKY,EAGH,OAFAJ,EAAQ,WACRG,EAAc,GAGhBH,EAAQ,MACRG,EAAc,IACb,CAACC,EAAYzG,EAASH,IAEzBgG,EAAAA,UAAU,KACR,IAAKY,EACH,OAEF,MAAMO,EAAa,IAAIC,gBAgBvB,OAfA3E,EAAetC,EAAmB,CAChCH,UACAiC,OAAQkF,EAAWlF,OACnBc,YAAa2D,EAAa,IAEzBW,KAAMC,IACLd,EAAQc,GACR,MAAAnC,GAAAA,EAAcmC,KAEfC,MAAOnE,IACY,gBAAd,MAAAA,OAAA,EAAAA,EAAKC,OAEPmE,QAAQC,KAAK,2BAA4BrE,KAGxC,IAAM+D,EAAWO,SACvB,CAACd,EAAYzG,EAASH,EAASmF,EAAauB,IAE/CV,EAAAA,UAAU,KAKRgB,GAJKJ,GAA8B,SAAhBC,IAKlB,CAACD,EAAYzG,EAAS0G,IAEzBb,EAAAA,UAAU,KACR,IAAKY,GAA8B,SAAhBC,GAA0BE,EAC3C,OAEF,GAAsB,oBAAXX,aAAiE,IAAhCA,OAAOuB,qBAEjD,YADAX,GAAY,GAGd,MAAMY,EAAOhC,EAAOG,QACpB,IAAK6B,EACH,OAEF,MAAMC,EAAW,IAAIF,qBAAsBG,IACrCA,EAAQvH,KAAME,GAAUA,EAAMsH,kBAChCf,GAAY,GACZa,EAASG,eAEV,CAAEC,WAAY,UAEjB,OADAJ,EAASK,QAAQN,GACV,IAAMC,EAASG,cACrB,CAACpB,EAAYC,EAAaE,IAG7B,MAAMoB,EAAUjB,EAAAA,QAAQ,eACtB,IAAK/F,EAAM,OAAO,KAClB,MAAMiD,GAAI,OAAAhD,EAAAD,EAAKE,oBAAL,EAAAD,EAAoBd,WAAY,CAAA,EACpC8H,EAAQhE,EAAUiE,KAClBC,EAAQlE,EAAUmE,KAClBC,EAAQpE,EAAUqE,KAElBnE,EACJH,EAAsB,OAAAuE,EAAAtE,EAAUiE,eAAMM,WACtCxE,EAAsB,OAAAyE,EAAAxE,EAAUqE,WAAV,EAAAG,EAAgBD,WACtC,IAAK1E,GAUD4E,EAAsBC,GARL,CAAC9G,IACtB,GAAKkC,EAAYlC,GACjB,MAAI,gBAAgB+G,KAAK/G,IAAQA,EAAIgH,WAAW,SAAiBhH,EAC7DA,EAAIgH,WAAW,MAAc,SAAShH,IACtCA,EAAIgH,WAAW,KAAa,GAAG/B,IAAYjF,IACxC,GAAGiF,KAAajF,KAIvBiH,CAAoC,iBAAdH,EAAyBA,OAAY,GAEvDI,EAAoB,CACxBvE,EAASyD,GACTzD,EAAS6D,GACT7D,EAAS2D,GACTF,MAAAA,OAAAA,EAAAA,EAAMzH,GACNyH,MAAAA,OAAAA,EAAAA,EAAMxH,GACNwH,MAAAA,OAAAA,EAAAA,EAAMvH,GACNuH,MAAAA,OAAAA,EAAAA,EAAMtH,KACN0H,MAAAA,OAAAA,EAAAA,EAAM7H,GACN6H,MAAAA,OAAAA,EAAAA,EAAM5H,GACN4H,MAAAA,OAAAA,EAAAA,EAAM3H,GACN2H,MAAAA,OAAAA,EAAAA,EAAM1H,KACNwH,MAAAA,OAAAA,EAAAA,EAAM3H,GACN2H,MAAAA,OAAAA,EAAAA,EAAM1H,GACN0H,MAAAA,OAAAA,EAAAA,EAAMzH,GACNyH,MAAAA,OAAAA,EAAAA,EAAMxH,MAELqI,IAAKL,GAAcD,EAAmBC,QAAa,IACnDM,OAAOlF,GAEJ5C,EAAMH,EACNkI,EAAmB,CACvB/H,EAAIC,QACJD,EAAIE,cACJF,EAAIG,wBACJH,EAAII,QACJJ,EAAIK,QACJL,EAAIM,UACJN,EAAIO,eAEHsH,IAAKL,GAAe5E,EAAY4E,GAAaD,EAAmBC,QAAa,GAC7EM,OAAOlF,GAGJoF,EAAqBhI,EAAIiI,aAAe,CAACV,EAAmBvH,EAAIiI,eAAeH,OAAOlF,GAAe,GAErGsF,EAAW,IAAIN,KAAsBG,KAAqBC,GAAoB,GAEpF,IAAKE,EACH,OAAO,KAiBT,MAAO,CAAEpB,KAAAA,EAAME,KAAAA,EAAME,KAAAA,EAAMiB,SAdT7E,IAChB,IAAKA,EAAO,OACZ,MAAMkD,EAAoB,GACpB4B,EAAO,CAAC1H,EAAc2H,KAC1B,MAAMC,EAAWf,EAAmB7G,GAChC4H,GAAYD,GAAO7B,EAAQ4B,KAAK,GAAGE,KAAYD,OAMrD,OAJAD,EAAK9E,EAAMjE,GAAI2D,EAAO3D,IACtB+I,EAAK9E,EAAMhE,GAAI0D,EAAO1D,IACtB8I,EAAK9E,EAAM/D,GAAIyD,EAAOzD,IACtB6I,EAAK9E,EAAM9D,KAAMwD,EAAOxD,MACjBgH,EAAQ9G,OAAS8G,EAAQ+B,KAAK,WAAQ,GAGVL,SAAAA,EAAUlF,SAAQwF,iBAAkBZ,EAAkBlI,OAAS,IACnG,CAACG,EAAM8F,IAEJ8C,EAAoB7C,EAAAA,QACxB,WAAM,OAAA7G,GAAkC,OAAAe,EAAA,MAAAD,OAAA,EAAAA,EAAME,oBAAN,EAAAD,EAAqBd,WAAY,OACzE,CAACa,IAGG6I,EAAiB9C,EAAAA,QAAQ,WAC7B,MAAM3E,GAAU,OAAAnB,EAAA,MAAAD,OAAA,EAAAA,EAAME,oBAAN,EAAAD,EAAqBmB,gBAAUpB,WAAM8I,kBAAoB,GACzE,MAAyB,iBAAX1H,EAAsBA,EAAO2H,cAAgB,IAC1D,CAAC/I,IAEEgJ,EAAoC,WAAnBH,GAAkD,UAAnBA,EAEhDI,EACJxD,GAAc5B,QAAQ7D,KAAUgJ,IAAmBJ,GAAqBrD,EAxMvC,EA0MnCV,EAAAA,UAAU,KACR,IAAKoE,EACH,OAEF,GAAsB,oBAAXhE,OACT,OAEF,MAAMiE,EAAYjE,OAAOkE,WAAW,KAClC3D,EAAe4D,GAAUA,EAAQ,IAjNN,KAmN7B,MAAO,IAAMnE,OAAOoE,aAAaH,IAChC,CAACD,IAGJ,MAAMK,EAAUvD,EAAAA,QAAQ,aACtB,MAAmB,iBAAR5B,EAAyBA,GAC5B,OAAAoD,EAAA,OAAAtH,EAAA,MAAAD,OAAA,EAAAA,EAAMuJ,WAAN,EAAAtJ,EAAoBuJ,2BAAkBC,eAAW,GACxD,CAACtF,EAAKnE,IAEH0J,EAAY3D,EAAAA,QAAQ,aACxB,MAAqB,iBAAV3B,EAA2BA,GAC9B,OAAAmD,EAAA,OAAAtH,EAAA,MAAAD,OAAA,EAAAA,EAAMuJ,WAAN,EAAAtJ,EAAoBuJ,2BAAkBpF,aAAS,GACtD,CAACA,EAAOpE,IAEL2J,EAAY5D,EAAAA,QAAQ,iBAAM,OAAA,OAAAwB,EAAA,OAAAtH,EAAA,MAAAD,OAAA,EAAAA,EAAMuJ,WAAN,EAAAtJ,EAAY2J,aAAZ,EAAArC,EAAoBiB,SAAS,OAAAqB,EAAA,0BAAM3J,oBAAN,EAAAuH,EAAqBD,eAArB,EAAAqC,EAA+BrB,aAAS,GAAW,CAACxI,IAC3G8J,EAAa/D,EAAAA,QAAQ,iBAAM,OAAA,OAAAwB,EAAA,OAAAtH,EAAA,MAAAD,OAAA,EAAAA,EAAMuJ,WAAN,EAAAtJ,EAAY2J,aAAZ,EAAArC,EAAoBwC,UAAU,OAAAF,EAAA,0BAAM3J,oBAAN,EAAAuH,EAAqBD,eAArB,EAAAqC,EAA+BE,cAAU,GAAW,CAAC/J,IAG9GgK,EAAejE,EAAAA,QAAQ,aAC3B,MAAMkE,EAAQ,OAAA1C,EAAA,OAAAtH,EAAA,MAAAD,OAAA,EAAAA,EAAMuJ,WAAN,EAAAtJ,EAAoBuJ,uBAApB,EAAAjC,EAAsC2C,mBACpD,IAAKD,EAAM,OAEX,MAAME,EAAO,MAAAnD,OAAA,EAAAA,EAASqB,SACtB,IAAK8B,EAAM,OACX,MAAMC,EAAMD,EAAKE,YAAY,KAE7B,MAAO,GAAGJ,KADEG,GAAM,EAAKD,EAAKG,MAAMF,EAAM,GAAGrB,cAAgB,SAE1D,CAAC/I,EAAMgH,IAGV,IAAKvB,EAAY,CACf,MAAM8E,EAAS,IAAKhG,GACpB,OACEiG,EAAAC,cAAC,MAAA,CACCjG,IAAKC,EACLJ,IAAKC,EACLL,QAASyB,EACTxB,SAAUyB,EACVxB,IAAKmF,EACLlF,MAAOsF,EACPlB,MAAO+B,EAAE/B,MACTuB,OAAQQ,EAAER,UACNQ,GAGV,CAEA,MAAMG,EHzQD,SAAkC1L,EAAiBH,GAExD,MAAO,GADMD,EAAcC,2BACYG,GACzC,CGsQyB2L,CAAyB3L,EAAmBH,GAEnE,IAAKmB,IAASgH,IAAYpB,EAAU,CAClC,MAAM2E,EAAS,IAAKhG,GACpB,OACEiG,EAAAC,cAAC,MAAA,CACCjG,IAAKC,EACLJ,IAAKqG,EACLzG,QAASyB,EACTxB,SAAUyB,EACVxB,IAAKmF,EACLlF,MAAOsF,EACPlB,MAAO+B,EAAE/B,OAASmB,EAClBI,OAAQQ,EAAER,QAAUD,KAChBS,GAGV,CAEA,MAAMK,EAAYnH,GAjQE,8DAkQdwD,KAAEA,EAAAE,KAAMA,EAAAE,KAAMA,EAAAiB,SAAMA,EAAAD,SAAUA,GAAarB,EAE3C6D,EAAUvC,EAASrB,GACnB6D,EAAUxC,EAASnB,GACnB4D,EAAUzC,EAASjB,GAEzB,OAAIwD,GAAWC,GAAWC,EAEtBP,EAAAC,cAAC,eACEK,EAAUN,EAAAC,cAAC,UAAOO,KAAK,aAAaC,OAAQH,EAASrH,MAAOmH,IAAgB,KAC5EC,EAAUL,EAAAC,cAAC,SAAA,CAAOO,KAAK,aAAaC,OAAQJ,EAASpH,MAAOmH,IAAgB,KAC7EJ,EAAAC,cAAC,MAAA,CACCjG,IAAKC,EACLJ,IAAKgE,EACL4C,QAAQF,GAAYF,GAAYC,OAAoB,EAAVC,EAC1CtH,OAAOsH,GAAYF,GAAYC,OAAsB,EAAZF,EACzC3G,QAASyB,EACTxB,SAAUyB,EACVxB,IAAKmF,EACLlF,MAAOsF,EACPlB,MAAOmB,EACPI,OAAQD,EACR,gBAAeE,KACXzF,KAOViG,EAAAC,cAAC,MAAA,CACCjG,IAAKC,EACLJ,IAAKgE,EACLpE,QAASyB,EACTxB,SAAUyB,EACVxB,IAAKmF,EACLlF,MAAOsF,EACPlB,MAAOmB,EACPI,OAAQD,EACR,gBAAeE,KACXzF,GAGV,GAEa2G,EAAMC,EAAAA,KAAKrH,GACxBoH,EAAIE,YAAc,cCjWlB,MAAMC,EACkB,oBAAfC,WAA8BA,gBAAmC,EAE1E,GAAID,EACF,GAAKA,EAAaE,QAIX,CACL,MAAMC,EAAMH,EAAaE,QAAQC,MAAQH,EAAaE,QAAQC,IAAM,SACxC,IAAjBA,EAAIC,WACbD,EAAIC,SAAW,aAEnB,MAREJ,EAAaE,QAAU,CACrBC,IAAK,CAAEC,SAAU"}
@@ -0,0 +1,248 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { forwardRef, memo, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
4
+ import { DEFAULT_CDN_HOST, buildPlaceholderImageUrl, fetchImageData, imageVariantsHaveRenderableSource, } from '../utils/api.js';
5
+ import { useMediaSelectionEffect } from './useMediaSelectionEffect.js';
6
+ import { useResponsiveReset } from './useResponsiveReset.js';
7
+ const DEFAULT_WIDTHS = {
8
+ sm: 640,
9
+ md: 1024,
10
+ lg: 1536,
11
+ full: 2560,
12
+ };
13
+ const MAX_VARIANT_REFRESH_ATTEMPTS = 5;
14
+ const VARIANT_REFRESH_DELAY_MS = 3000;
15
+ const isUrlString = (value) => typeof value === 'string' && value.trim().length > 0;
16
+ function widthMapFromMetadata(v) {
17
+ const w = v?.widths;
18
+ if (!w)
19
+ return null;
20
+ return {
21
+ sm: w.small ?? w.sm ?? DEFAULT_WIDTHS.sm,
22
+ md: w.medium ?? w.md ?? DEFAULT_WIDTHS.md,
23
+ lg: w.large ?? w.lg ?? DEFAULT_WIDTHS.lg,
24
+ full: w.full_size ?? w.full ?? DEFAULT_WIDTHS.full,
25
+ };
26
+ }
27
+ function pickBest(sizes) {
28
+ if (!sizes)
29
+ return undefined;
30
+ return sizes.md || sizes.lg || sizes.sm || sizes.full || Object.values(sizes).find(Boolean);
31
+ }
32
+ const DEFAULT_SIZES = '(max-width:640px) 640px, (max-width:1024px) 1024px, 1536px';
33
+ const ImgBase = forwardRef(function Img({ mediaId, cdnHost, sizes, onImageData, loading, decoding, alt, title, src: directSrc, ...rest }, ref) {
34
+ const imgRef = useRef(null);
35
+ useImperativeHandle(ref, () => imgRef.current);
36
+ const pictureRef = useRef(null);
37
+ useResponsiveReset(pictureRef);
38
+ useMediaSelectionEffect();
39
+ const [data, setData] = useState(null);
40
+ const [retryCount, setRetryCount] = useState(0);
41
+ const hasMediaId = Number.isFinite(mediaId);
42
+ const loadingAttr = loading ?? 'lazy';
43
+ const decodingAttr = decoding ?? 'async';
44
+ const [isInView, setIsInView] = useState(() => !hasMediaId || loadingAttr !== 'lazy');
45
+ const cdnOrigin = useMemo(() => (cdnHost ?? DEFAULT_CDN_HOST).replace(/\/$/, ''), [cdnHost]);
46
+ useEffect(() => {
47
+ if (!hasMediaId) {
48
+ setData(null);
49
+ setRetryCount(0);
50
+ return;
51
+ }
52
+ setData(null);
53
+ setRetryCount(0);
54
+ }, [hasMediaId, mediaId, cdnHost]);
55
+ useEffect(() => {
56
+ if (!hasMediaId) {
57
+ return;
58
+ }
59
+ const controller = new AbortController();
60
+ fetchImageData(mediaId, {
61
+ cdnHost,
62
+ signal: controller.signal,
63
+ bypassCache: retryCount > 0,
64
+ })
65
+ .then((d) => {
66
+ setData(d);
67
+ onImageData?.(d);
68
+ })
69
+ .catch((err) => {
70
+ if (err?.name !== 'AbortError') {
71
+ // eslint-disable-next-line no-console
72
+ console.warn('Image data fetch failed:', err);
73
+ }
74
+ });
75
+ return () => controller.abort();
76
+ }, [hasMediaId, mediaId, cdnHost, onImageData, retryCount]);
77
+ useEffect(() => {
78
+ if (!hasMediaId || loadingAttr !== 'lazy') {
79
+ setIsInView(true);
80
+ return;
81
+ }
82
+ setIsInView(false);
83
+ }, [hasMediaId, mediaId, loadingAttr]);
84
+ useEffect(() => {
85
+ if (!hasMediaId || loadingAttr !== 'lazy' || isInView) {
86
+ return;
87
+ }
88
+ if (typeof window === 'undefined' || typeof window.IntersectionObserver === 'undefined') {
89
+ setIsInView(true);
90
+ return;
91
+ }
92
+ const node = imgRef.current;
93
+ if (!node) {
94
+ return;
95
+ }
96
+ const observer = new IntersectionObserver((entries) => {
97
+ if (entries.some((entry) => entry.isIntersecting)) {
98
+ setIsInView(true);
99
+ observer.disconnect();
100
+ }
101
+ }, { rootMargin: '200px' });
102
+ observer.observe(node);
103
+ return () => observer.disconnect();
104
+ }, [hasMediaId, loadingAttr, isInView]);
105
+ // Build picture/source/srcset from variants
106
+ const picture = useMemo(() => {
107
+ if (!data)
108
+ return null;
109
+ const v = data.variants_data?.variants ?? {};
110
+ const webp = v.WEBP;
111
+ const avif = v.AVIF;
112
+ const jpeg = v.JPEG;
113
+ const widths = widthMapFromMetadata(v.WEBP?.metadata) ||
114
+ widthMapFromMetadata(v.JPEG?.metadata) ||
115
+ { ...DEFAULT_WIDTHS };
116
+ const ensureAbsolute = (url) => {
117
+ if (!isUrlString(url))
118
+ return undefined;
119
+ if (/^https?:\/\//i.test(url) || url.startsWith('data:'))
120
+ return url;
121
+ if (url.startsWith('//'))
122
+ return `https:${url}`;
123
+ if (url.startsWith('/'))
124
+ return `${cdnOrigin}${url}`;
125
+ return `${cdnOrigin}/${url}`;
126
+ };
127
+ const normalizeCandidate = (candidate) => ensureAbsolute(typeof candidate === 'string' ? candidate : undefined);
128
+ const variantCandidates = [
129
+ pickBest(webp),
130
+ pickBest(jpeg),
131
+ pickBest(avif),
132
+ webp?.sm,
133
+ webp?.md,
134
+ webp?.lg,
135
+ webp?.full,
136
+ jpeg?.sm,
137
+ jpeg?.md,
138
+ jpeg?.lg,
139
+ jpeg?.full,
140
+ avif?.sm,
141
+ avif?.md,
142
+ avif?.lg,
143
+ avif?.full,
144
+ ]
145
+ .map((candidate) => normalizeCandidate(candidate ?? undefined))
146
+ .filter(isUrlString);
147
+ const raw = data;
148
+ const directCandidates = [
149
+ raw.img_url,
150
+ raw.file_data_url,
151
+ raw.file_data_thumbnail_url,
152
+ raw.img_src,
153
+ raw.med_src,
154
+ raw.thumb_src,
155
+ raw.low_res_thumb,
156
+ ]
157
+ .map((candidate) => (isUrlString(candidate) ? normalizeCandidate(candidate) : undefined))
158
+ .filter(isUrlString);
159
+ // Add fallback_url as the final option if no variants or direct candidates
160
+ const fallbackCandidates = raw.fallback_url ? [normalizeCandidate(raw.fallback_url)].filter(isUrlString) : [];
161
+ const fallback = [...variantCandidates, ...directCandidates, ...fallbackCandidates][0];
162
+ if (!fallback) {
163
+ return null;
164
+ }
165
+ const toSrcSet = (sizes) => {
166
+ if (!sizes)
167
+ return undefined;
168
+ const entries = [];
169
+ const push = (url, width) => {
170
+ const absolute = normalizeCandidate(url);
171
+ if (absolute && width)
172
+ entries.push(`${absolute} ${width}w`);
173
+ };
174
+ push(sizes.sm, widths.sm);
175
+ push(sizes.md, widths.md);
176
+ push(sizes.lg, widths.lg);
177
+ push(sizes.full, widths.full);
178
+ return entries.length ? entries.join(', ') : undefined;
179
+ };
180
+ return { webp, avif, jpeg, toSrcSet, fallback, widths, hasVariantSource: variantCandidates.length > 0 };
181
+ }, [data, cdnOrigin]);
182
+ const hasVariantEntries = useMemo(() => imageVariantsHaveRenderableSource(data?.variants_data?.variants ?? null), [data]);
183
+ const variantsStatus = useMemo(() => {
184
+ const status = (data?.variants_data?.status ?? data?.variants_status) ?? '';
185
+ return typeof status === 'string' ? status.toLowerCase() : '';
186
+ }, [data]);
187
+ const variantsFailed = variantsStatus === 'failed' || variantsStatus === 'error';
188
+ const shouldPollForVariants = hasMediaId && Boolean(data) && !variantsFailed && !hasVariantEntries && retryCount < MAX_VARIANT_REFRESH_ATTEMPTS;
189
+ useEffect(() => {
190
+ if (!shouldPollForVariants) {
191
+ return;
192
+ }
193
+ if (typeof window === 'undefined') {
194
+ return;
195
+ }
196
+ const timeoutId = window.setTimeout(() => {
197
+ setRetryCount((count) => count + 1);
198
+ }, VARIANT_REFRESH_DELAY_MS);
199
+ return () => window.clearTimeout(timeoutId);
200
+ }, [shouldPollForVariants]);
201
+ // Map HTML attributes from content manifest and sizing
202
+ const altAttr = useMemo(() => {
203
+ if (typeof alt === 'string')
204
+ return alt;
205
+ return data?.meta?.content_manifest?.summary ?? undefined;
206
+ }, [alt, data]);
207
+ const titleAttr = useMemo(() => {
208
+ if (typeof title === 'string')
209
+ return title;
210
+ return data?.meta?.content_manifest?.title ?? undefined;
211
+ }, [title, data]);
212
+ const widthAttr = useMemo(() => data?.meta?.sizing?.width ?? data?.variants_data?.metadata?.width ?? undefined, [data]);
213
+ const heightAttr = useMemo(() => data?.meta?.sizing?.height ?? data?.variants_data?.metadata?.height ?? undefined, [data]);
214
+ // Compute data-filename for consumers that need semantic filenames
215
+ const dataFilename = useMemo(() => {
216
+ const base = data?.meta?.content_manifest?.optimized_filename;
217
+ if (!base)
218
+ return undefined;
219
+ // ext derived from chosen fallback url
220
+ const href = picture?.fallback;
221
+ if (!href)
222
+ return undefined;
223
+ const dot = href.lastIndexOf('.');
224
+ const ext = dot > -1 ? href.slice(dot + 1).toLowerCase() : 'jpg';
225
+ return `${base}.${ext}`;
226
+ }, [data, picture]);
227
+ // If mediaId not provided but src is, render plain img
228
+ if (!hasMediaId) {
229
+ const r = { ...rest };
230
+ return (_jsx("img", { ref: imgRef, src: directSrc, loading: loadingAttr, decoding: decodingAttr, alt: altAttr, title: titleAttr, width: r.width, height: r.height, ...r }));
231
+ }
232
+ const placeholderSrc = buildPlaceholderImageUrl(mediaId, cdnHost);
233
+ if (!data || !picture || !isInView) {
234
+ const r = { ...rest };
235
+ return (_jsx("img", { ref: imgRef, src: placeholderSrc, loading: loadingAttr, decoding: decodingAttr, alt: altAttr, title: titleAttr, width: r.width ?? widthAttr, height: r.height ?? heightAttr, ...r }));
236
+ }
237
+ const sizesAttr = sizes ?? DEFAULT_SIZES;
238
+ const { webp, avif, jpeg, toSrcSet, fallback } = picture;
239
+ const webpSet = toSrcSet(webp);
240
+ const avifSet = toSrcSet(avif);
241
+ const jpegSet = toSrcSet(jpeg);
242
+ if (webpSet || avifSet || jpegSet) {
243
+ return (_jsxs("picture", { children: [avifSet ? _jsx("source", { type: "image/avif", srcSet: avifSet, sizes: sizesAttr }) : null, webpSet ? _jsx("source", { type: "image/webp", srcSet: webpSet, sizes: sizesAttr }) : null, _jsx("img", { ref: imgRef, src: fallback, srcSet: jpegSet && !webpSet && !avifSet ? jpegSet : undefined, sizes: jpegSet && !webpSet && !avifSet ? sizesAttr : undefined, loading: loadingAttr, decoding: decodingAttr, alt: altAttr, title: titleAttr, width: widthAttr, height: heightAttr, "data-filename": dataFilename, ...rest })] }));
244
+ }
245
+ return (_jsx("img", { ref: imgRef, src: fallback, loading: loadingAttr, decoding: decodingAttr, alt: altAttr, title: titleAttr, width: widthAttr, height: heightAttr, "data-filename": dataFilename, ...rest }));
246
+ });
247
+ export const Img = memo(ImgBase);
248
+ Img.displayName = 'OpenSiteImg';
@@ -0,0 +1,20 @@
1
+ import React from 'react';
2
+ import type { ImageData } from '../types.js';
3
+ type NativeImgProps = Omit<React.ImgHTMLAttributes<HTMLImageElement>, 'src' | 'srcSet' | 'sizes'> & {
4
+ src?: string;
5
+ };
6
+ export type ImgProps = NativeImgProps & {
7
+ mediaId?: number;
8
+ cdnHost?: string;
9
+ sizes?: string;
10
+ onImageData?: (data: ImageData) => void;
11
+ };
12
+ export declare const Img: React.MemoExoticComponent<React.ForwardRefExoticComponent<Omit<React.ImgHTMLAttributes<HTMLImageElement>, "src" | "srcSet" | "sizes"> & {
13
+ src?: string;
14
+ } & {
15
+ mediaId?: number;
16
+ cdnHost?: string;
17
+ sizes?: string;
18
+ onImageData?: (data: ImageData) => void;
19
+ } & React.RefAttributes<HTMLImageElement>>>;
20
+ export {};
@@ -0,0 +1,248 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { forwardRef, memo, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
4
+ import { DEFAULT_CDN_HOST, buildPlaceholderImageUrl, fetchImageData, imageVariantsHaveRenderableSource, } from '../utils/api.js';
5
+ import { useMediaSelectionEffect } from './useMediaSelectionEffect.js';
6
+ import { useResponsiveReset } from './useResponsiveReset.js';
7
+ const DEFAULT_WIDTHS = {
8
+ sm: 640,
9
+ md: 1024,
10
+ lg: 1536,
11
+ full: 2560,
12
+ };
13
+ const MAX_VARIANT_REFRESH_ATTEMPTS = 5;
14
+ const VARIANT_REFRESH_DELAY_MS = 3000;
15
+ const isUrlString = (value) => typeof value === 'string' && value.trim().length > 0;
16
+ function widthMapFromMetadata(v) {
17
+ const w = v?.widths;
18
+ if (!w)
19
+ return null;
20
+ return {
21
+ sm: w.small ?? w.sm ?? DEFAULT_WIDTHS.sm,
22
+ md: w.medium ?? w.md ?? DEFAULT_WIDTHS.md,
23
+ lg: w.large ?? w.lg ?? DEFAULT_WIDTHS.lg,
24
+ full: w.full_size ?? w.full ?? DEFAULT_WIDTHS.full,
25
+ };
26
+ }
27
+ function pickBest(sizes) {
28
+ if (!sizes)
29
+ return undefined;
30
+ return sizes.md || sizes.lg || sizes.sm || sizes.full || Object.values(sizes).find(Boolean);
31
+ }
32
+ const DEFAULT_SIZES = '(max-width:640px) 640px, (max-width:1024px) 1024px, 1536px';
33
+ const ImgBase = forwardRef(function Img({ mediaId, cdnHost, sizes, onImageData, loading, decoding, alt, title, src: directSrc, ...rest }, ref) {
34
+ const imgRef = useRef(null);
35
+ useImperativeHandle(ref, () => imgRef.current);
36
+ const pictureRef = useRef(null);
37
+ useResponsiveReset(pictureRef);
38
+ useMediaSelectionEffect();
39
+ const [data, setData] = useState(null);
40
+ const [retryCount, setRetryCount] = useState(0);
41
+ const hasMediaId = Number.isFinite(mediaId);
42
+ const loadingAttr = loading ?? 'lazy';
43
+ const decodingAttr = decoding ?? 'async';
44
+ const [isInView, setIsInView] = useState(() => !hasMediaId || loadingAttr !== 'lazy');
45
+ const cdnOrigin = useMemo(() => (cdnHost ?? DEFAULT_CDN_HOST).replace(/\/$/, ''), [cdnHost]);
46
+ useEffect(() => {
47
+ if (!hasMediaId) {
48
+ setData(null);
49
+ setRetryCount(0);
50
+ return;
51
+ }
52
+ setData(null);
53
+ setRetryCount(0);
54
+ }, [hasMediaId, mediaId, cdnHost]);
55
+ useEffect(() => {
56
+ if (!hasMediaId) {
57
+ return;
58
+ }
59
+ const controller = new AbortController();
60
+ fetchImageData(mediaId, {
61
+ cdnHost,
62
+ signal: controller.signal,
63
+ bypassCache: retryCount > 0,
64
+ })
65
+ .then((d) => {
66
+ setData(d);
67
+ onImageData?.(d);
68
+ })
69
+ .catch((err) => {
70
+ if (err?.name !== 'AbortError') {
71
+ // eslint-disable-next-line no-console
72
+ console.warn('Image data fetch failed:', err);
73
+ }
74
+ });
75
+ return () => controller.abort();
76
+ }, [hasMediaId, mediaId, cdnHost, onImageData, retryCount]);
77
+ useEffect(() => {
78
+ if (!hasMediaId || loadingAttr !== 'lazy') {
79
+ setIsInView(true);
80
+ return;
81
+ }
82
+ setIsInView(false);
83
+ }, [hasMediaId, mediaId, loadingAttr]);
84
+ useEffect(() => {
85
+ if (!hasMediaId || loadingAttr !== 'lazy' || isInView) {
86
+ return;
87
+ }
88
+ if (typeof window === 'undefined' || typeof window.IntersectionObserver === 'undefined') {
89
+ setIsInView(true);
90
+ return;
91
+ }
92
+ const node = imgRef.current;
93
+ if (!node) {
94
+ return;
95
+ }
96
+ const observer = new IntersectionObserver((entries) => {
97
+ if (entries.some((entry) => entry.isIntersecting)) {
98
+ setIsInView(true);
99
+ observer.disconnect();
100
+ }
101
+ }, { rootMargin: '200px' });
102
+ observer.observe(node);
103
+ return () => observer.disconnect();
104
+ }, [hasMediaId, loadingAttr, isInView]);
105
+ // Build picture/source/srcset from variants
106
+ const picture = useMemo(() => {
107
+ if (!data)
108
+ return null;
109
+ const v = data.variants_data?.variants ?? {};
110
+ const webp = v.WEBP;
111
+ const avif = v.AVIF;
112
+ const jpeg = v.JPEG;
113
+ const widths = widthMapFromMetadata(v.WEBP?.metadata) ||
114
+ widthMapFromMetadata(v.JPEG?.metadata) ||
115
+ { ...DEFAULT_WIDTHS };
116
+ const ensureAbsolute = (url) => {
117
+ if (!isUrlString(url))
118
+ return undefined;
119
+ if (/^https?:\/\//i.test(url) || url.startsWith('data:'))
120
+ return url;
121
+ if (url.startsWith('//'))
122
+ return `https:${url}`;
123
+ if (url.startsWith('/'))
124
+ return `${cdnOrigin}${url}`;
125
+ return `${cdnOrigin}/${url}`;
126
+ };
127
+ const normalizeCandidate = (candidate) => ensureAbsolute(typeof candidate === 'string' ? candidate : undefined);
128
+ const variantCandidates = [
129
+ pickBest(webp),
130
+ pickBest(jpeg),
131
+ pickBest(avif),
132
+ webp?.sm,
133
+ webp?.md,
134
+ webp?.lg,
135
+ webp?.full,
136
+ jpeg?.sm,
137
+ jpeg?.md,
138
+ jpeg?.lg,
139
+ jpeg?.full,
140
+ avif?.sm,
141
+ avif?.md,
142
+ avif?.lg,
143
+ avif?.full,
144
+ ]
145
+ .map((candidate) => normalizeCandidate(candidate ?? undefined))
146
+ .filter(isUrlString);
147
+ const raw = data;
148
+ const directCandidates = [
149
+ raw.img_url,
150
+ raw.file_data_url,
151
+ raw.file_data_thumbnail_url,
152
+ raw.img_src,
153
+ raw.med_src,
154
+ raw.thumb_src,
155
+ raw.low_res_thumb,
156
+ ]
157
+ .map((candidate) => (isUrlString(candidate) ? normalizeCandidate(candidate) : undefined))
158
+ .filter(isUrlString);
159
+ // Add fallback_url as the final option if no variants or direct candidates
160
+ const fallbackCandidates = raw.fallback_url ? [normalizeCandidate(raw.fallback_url)].filter(isUrlString) : [];
161
+ const fallback = [...variantCandidates, ...directCandidates, ...fallbackCandidates][0];
162
+ if (!fallback) {
163
+ return null;
164
+ }
165
+ const toSrcSet = (sizes) => {
166
+ if (!sizes)
167
+ return undefined;
168
+ const entries = [];
169
+ const push = (url, width) => {
170
+ const absolute = normalizeCandidate(url);
171
+ if (absolute && width)
172
+ entries.push(`${absolute} ${width}w`);
173
+ };
174
+ push(sizes.sm, widths.sm);
175
+ push(sizes.md, widths.md);
176
+ push(sizes.lg, widths.lg);
177
+ push(sizes.full, widths.full);
178
+ return entries.length ? entries.join(', ') : undefined;
179
+ };
180
+ return { webp, avif, jpeg, toSrcSet, fallback, widths, hasVariantSource: variantCandidates.length > 0 };
181
+ }, [data, cdnOrigin]);
182
+ const hasVariantEntries = useMemo(() => imageVariantsHaveRenderableSource(data?.variants_data?.variants ?? null), [data]);
183
+ const variantsStatus = useMemo(() => {
184
+ const status = (data?.variants_data?.status ?? data?.variants_status) ?? '';
185
+ return typeof status === 'string' ? status.toLowerCase() : '';
186
+ }, [data]);
187
+ const variantsFailed = variantsStatus === 'failed' || variantsStatus === 'error';
188
+ const shouldPollForVariants = hasMediaId && Boolean(data) && !variantsFailed && !hasVariantEntries && retryCount < MAX_VARIANT_REFRESH_ATTEMPTS;
189
+ useEffect(() => {
190
+ if (!shouldPollForVariants) {
191
+ return;
192
+ }
193
+ if (typeof window === 'undefined') {
194
+ return;
195
+ }
196
+ const timeoutId = window.setTimeout(() => {
197
+ setRetryCount((count) => count + 1);
198
+ }, VARIANT_REFRESH_DELAY_MS);
199
+ return () => window.clearTimeout(timeoutId);
200
+ }, [shouldPollForVariants]);
201
+ // Map HTML attributes from content manifest and sizing
202
+ const altAttr = useMemo(() => {
203
+ if (typeof alt === 'string')
204
+ return alt;
205
+ return data?.meta?.content_manifest?.summary ?? undefined;
206
+ }, [alt, data]);
207
+ const titleAttr = useMemo(() => {
208
+ if (typeof title === 'string')
209
+ return title;
210
+ return data?.meta?.content_manifest?.title ?? undefined;
211
+ }, [title, data]);
212
+ const widthAttr = useMemo(() => data?.meta?.sizing?.width ?? data?.variants_data?.metadata?.width ?? undefined, [data]);
213
+ const heightAttr = useMemo(() => data?.meta?.sizing?.height ?? data?.variants_data?.metadata?.height ?? undefined, [data]);
214
+ // Compute data-filename for consumers that need semantic filenames
215
+ const dataFilename = useMemo(() => {
216
+ const base = data?.meta?.content_manifest?.optimized_filename;
217
+ if (!base)
218
+ return undefined;
219
+ // ext derived from chosen fallback url
220
+ const href = picture?.fallback;
221
+ if (!href)
222
+ return undefined;
223
+ const dot = href.lastIndexOf('.');
224
+ const ext = dot > -1 ? href.slice(dot + 1).toLowerCase() : 'jpg';
225
+ return `${base}.${ext}`;
226
+ }, [data, picture]);
227
+ // If mediaId not provided but src is, render plain img
228
+ if (!hasMediaId) {
229
+ const r = { ...rest };
230
+ return (_jsx("img", { ref: imgRef, src: directSrc, loading: loadingAttr, decoding: decodingAttr, alt: altAttr, title: titleAttr, width: r.width, height: r.height, ...r }));
231
+ }
232
+ const placeholderSrc = buildPlaceholderImageUrl(mediaId, cdnHost);
233
+ if (!data || !picture || !isInView) {
234
+ const r = { ...rest };
235
+ return (_jsx("img", { ref: imgRef, src: placeholderSrc, loading: loadingAttr, decoding: decodingAttr, alt: altAttr, title: titleAttr, width: r.width ?? widthAttr, height: r.height ?? heightAttr, ...r }));
236
+ }
237
+ const sizesAttr = sizes ?? DEFAULT_SIZES;
238
+ const { webp, avif, jpeg, toSrcSet, fallback } = picture;
239
+ const webpSet = toSrcSet(webp);
240
+ const avifSet = toSrcSet(avif);
241
+ const jpegSet = toSrcSet(jpeg);
242
+ if (webpSet || avifSet || jpegSet) {
243
+ return (_jsxs("picture", { children: [avifSet ? _jsx("source", { type: "image/avif", srcSet: avifSet, sizes: sizesAttr }) : null, webpSet ? _jsx("source", { type: "image/webp", srcSet: webpSet, sizes: sizesAttr }) : null, _jsx("img", { ref: imgRef, src: fallback, srcSet: jpegSet && !webpSet && !avifSet ? jpegSet : undefined, sizes: jpegSet && !webpSet && !avifSet ? sizesAttr : undefined, loading: loadingAttr, decoding: decodingAttr, alt: altAttr, title: titleAttr, width: widthAttr, height: heightAttr, "data-filename": dataFilename, ...rest })] }));
244
+ }
245
+ return (_jsx("img", { ref: imgRef, src: fallback, loading: loadingAttr, decoding: decodingAttr, alt: altAttr, title: titleAttr, width: widthAttr, height: heightAttr, "data-filename": dataFilename, ...rest }));
246
+ });
247
+ export const Img = memo(ImgBase);
248
+ Img.displayName = 'OpenSiteImg';
@@ -0,0 +1 @@
1
+ export { Img } from './Img.js';
@@ -0,0 +1 @@
1
+ export { Img } from './Img.js';
@@ -0,0 +1 @@
1
+ export { Img } from './Img.js';
@@ -0,0 +1,20 @@
1
+ import { useEffect } from 'react';
2
+ const MEDIA_SELECTED_EVENT = 'dt:media-selected';
3
+ export function sendMediaSelection(blockId, payload) {
4
+ if (typeof window === 'undefined')
5
+ return;
6
+ window.dispatchEvent(new CustomEvent(MEDIA_SELECTED_EVENT, {
7
+ detail: { blockId, payload },
8
+ }));
9
+ }
10
+ export function useMediaSelectionEffect() {
11
+ useEffect(() => {
12
+ if (typeof window === 'undefined')
13
+ return;
14
+ const handler = () => {
15
+ // no-op: the real handler is attached in the builder via addEventListener
16
+ };
17
+ window.addEventListener(MEDIA_SELECTED_EVENT, handler);
18
+ return () => window.removeEventListener(MEDIA_SELECTED_EVENT, handler);
19
+ }, []);
20
+ }
@@ -0,0 +1,2 @@
1
+ export declare function sendMediaSelection(blockId: string, payload: unknown): void;
2
+ export declare function useMediaSelectionEffect(): void;
@@ -0,0 +1,20 @@
1
+ import { useEffect } from 'react';
2
+ const MEDIA_SELECTED_EVENT = 'dt:media-selected';
3
+ export function sendMediaSelection(blockId, payload) {
4
+ if (typeof window === 'undefined')
5
+ return;
6
+ window.dispatchEvent(new CustomEvent(MEDIA_SELECTED_EVENT, {
7
+ detail: { blockId, payload },
8
+ }));
9
+ }
10
+ export function useMediaSelectionEffect() {
11
+ useEffect(() => {
12
+ if (typeof window === 'undefined')
13
+ return;
14
+ const handler = () => {
15
+ // no-op: the real handler is attached in the builder via addEventListener
16
+ };
17
+ window.addEventListener(MEDIA_SELECTED_EVENT, handler);
18
+ return () => window.removeEventListener(MEDIA_SELECTED_EVENT, handler);
19
+ }, []);
20
+ }
@@ -0,0 +1,29 @@
1
+ import { useEffect } from 'react';
2
+ export function resetResponsivePictureState(element) {
3
+ if (!element)
4
+ return;
5
+ element.querySelectorAll('source').forEach((source) => {
6
+ // force browser to reconsider responsive sources
7
+ const srcset = source.getAttribute('srcset');
8
+ if (srcset) {
9
+ source.setAttribute('data-srcset', srcset);
10
+ source.removeAttribute('srcset');
11
+ requestAnimationFrame(() => {
12
+ source.setAttribute('srcset', srcset);
13
+ });
14
+ }
15
+ });
16
+ }
17
+ export function useResponsiveReset(ref) {
18
+ useEffect(() => {
19
+ const element = ref.current;
20
+ if (!element)
21
+ return;
22
+ if (element instanceof HTMLPictureElement) {
23
+ resetResponsivePictureState(element);
24
+ }
25
+ else if (element.parentElement instanceof HTMLPictureElement) {
26
+ resetResponsivePictureState(element.parentElement);
27
+ }
28
+ }, [ref]);
29
+ }
@@ -0,0 +1,2 @@
1
+ export declare function resetResponsivePictureState(element: HTMLPictureElement | null): void;
2
+ export declare function useResponsiveReset(ref: React.RefObject<HTMLPictureElement | HTMLImageElement>): void;
@@ -0,0 +1,29 @@
1
+ import { useEffect } from 'react';
2
+ export function resetResponsivePictureState(element) {
3
+ if (!element)
4
+ return;
5
+ element.querySelectorAll('source').forEach((source) => {
6
+ // force browser to reconsider responsive sources
7
+ const srcset = source.getAttribute('srcset');
8
+ if (srcset) {
9
+ source.setAttribute('data-srcset', srcset);
10
+ source.removeAttribute('srcset');
11
+ requestAnimationFrame(() => {
12
+ source.setAttribute('srcset', srcset);
13
+ });
14
+ }
15
+ });
16
+ }
17
+ export function useResponsiveReset(ref) {
18
+ useEffect(() => {
19
+ const element = ref.current;
20
+ if (!element)
21
+ return;
22
+ if (element instanceof HTMLPictureElement) {
23
+ resetResponsivePictureState(element);
24
+ }
25
+ else if (element.parentElement instanceof HTMLPictureElement) {
26
+ resetResponsivePictureState(element.parentElement);
27
+ }
28
+ }, [ref]);
29
+ }
package/dist/index.cjs ADDED
@@ -0,0 +1,16 @@
1
+ const globalObject = typeof globalThis !== 'undefined' ? globalThis : undefined;
2
+ if (globalObject) {
3
+ if (!globalObject.process) {
4
+ globalObject.process = {
5
+ env: { NODE_ENV: 'production' },
6
+ };
7
+ }
8
+ else {
9
+ const env = globalObject.process.env ?? (globalObject.process.env = {});
10
+ if (typeof env.NODE_ENV === 'undefined') {
11
+ env.NODE_ENV = 'production';
12
+ }
13
+ }
14
+ }
15
+ export * from './core/index.js';
16
+ export * from './types.js';
@@ -0,0 +1,2 @@
1
+ export * from './core/index.js';
2
+ export * from './types.js';
package/dist/index.js ADDED
@@ -0,0 +1,16 @@
1
+ const globalObject = typeof globalThis !== 'undefined' ? globalThis : undefined;
2
+ if (globalObject) {
3
+ if (!globalObject.process) {
4
+ globalObject.process = {
5
+ env: { NODE_ENV: 'production' },
6
+ };
7
+ }
8
+ else {
9
+ const env = globalObject.process.env ?? (globalObject.process.env = {});
10
+ if (typeof env.NODE_ENV === 'undefined') {
11
+ env.NODE_ENV = 'production';
12
+ }
13
+ }
14
+ }
15
+ export * from './core/index.js';
16
+ export * from './types.js';
package/dist/types.cjs ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,49 @@
1
+ export type ProgressiveSizes = Partial<Record<'sm' | 'md' | 'lg' | 'full', string>>;
2
+ export type ImageVariantsMap = Partial<Record<'AVIF' | 'WEBP' | 'JPEG', ProgressiveSizes & {
3
+ metadata?: {
4
+ widths?: {
5
+ small?: number;
6
+ medium?: number;
7
+ large?: number;
8
+ full_size?: number;
9
+ };
10
+ };
11
+ }>>;
12
+ export interface ImageVariantsData {
13
+ variants?: ImageVariantsMap | null;
14
+ metadata?: {
15
+ width?: number;
16
+ height?: number;
17
+ };
18
+ status?: string;
19
+ }
20
+ export interface ImageData {
21
+ id: number;
22
+ name?: string;
23
+ media_type?: string;
24
+ img_url?: string | null;
25
+ img_src?: string | null;
26
+ thumb_src?: string | null;
27
+ med_src?: string | null;
28
+ file_data_url?: string | null;
29
+ file_data_thumbnail_url?: string | null;
30
+ low_res_thumb?: string | null;
31
+ fallback_url?: string;
32
+ variants_status?: string | null;
33
+ meta?: {
34
+ sizing?: {
35
+ height?: number;
36
+ width?: number;
37
+ size_in_mb?: number;
38
+ aspect_ratio?: number;
39
+ };
40
+ semantic_filename?: string;
41
+ content_manifest?: {
42
+ title?: string;
43
+ summary?: string;
44
+ description?: string;
45
+ optimized_filename?: string;
46
+ };
47
+ };
48
+ variants_data?: ImageVariantsData | null;
49
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,94 @@
1
+ import { cacheGet, cacheSet } from './cache.js';
2
+ export const DEFAULT_CDN_HOST = 'https://cdn.ing';
3
+ function normalizeHost(cdnHost) {
4
+ return (cdnHost ?? DEFAULT_CDN_HOST).replace(/\/$/, '');
5
+ }
6
+ function buildPrimaryImageUrl(mediaId, cdnHost) {
7
+ const host = normalizeHost(cdnHost);
8
+ return `${host}/assets/images/${mediaId}`;
9
+ }
10
+ function buildLegacyImageUrl(mediaId, cdnHost) {
11
+ const host = normalizeHost(cdnHost);
12
+ return `${host}/i/r/${mediaId}`;
13
+ }
14
+ export function buildPlaceholderImageUrl(mediaId, cdnHost) {
15
+ const host = normalizeHost(cdnHost);
16
+ return `${host}/assets/low_res_thumb/${mediaId}`;
17
+ }
18
+ function hasRenderableUrlVariant(variant) {
19
+ if (!variant)
20
+ return false;
21
+ const candidates = [variant.sm, variant.md, variant.lg, variant.full];
22
+ return candidates.some((value) => typeof value === 'string' && value.trim().length > 0);
23
+ }
24
+ export function imageVariantsHaveRenderableSource(variants) {
25
+ if (!variants)
26
+ return false;
27
+ return ['AVIF', 'WEBP', 'JPEG'].some((format) => {
28
+ const entry = variants?.[format];
29
+ return hasRenderableUrlVariant(entry);
30
+ });
31
+ }
32
+ function hasUrlValue(value) {
33
+ return typeof value === 'string' && value.trim().length > 0;
34
+ }
35
+ export function imageDataHasRenderableSource(data) {
36
+ if (!data)
37
+ return false;
38
+ if (imageVariantsHaveRenderableSource(data.variants_data?.variants ?? null)) {
39
+ return true;
40
+ }
41
+ const raw = data;
42
+ const directFields = [
43
+ raw.img_url,
44
+ raw.file_data_url,
45
+ raw.file_data_thumbnail_url,
46
+ raw.img_src,
47
+ raw.med_src,
48
+ raw.thumb_src,
49
+ raw.low_res_thumb,
50
+ ];
51
+ return directFields.some(hasUrlValue);
52
+ }
53
+ async function fetchFrom(url, signal) {
54
+ const res = await fetch(url, { signal });
55
+ if (!res.ok) {
56
+ const error = new Error(`Failed to fetch image data (status ${res.status}) from ${url}`);
57
+ error.status = res.status;
58
+ throw error;
59
+ }
60
+ return (await res.json());
61
+ }
62
+ export async function fetchImageData(mediaId, options = {}) {
63
+ if (!Number.isFinite(mediaId)) {
64
+ throw new Error('Invalid mediaId provided to fetchImageData');
65
+ }
66
+ const host = normalizeHost(options.cdnHost);
67
+ const cacheKey = `image:${host}:${mediaId}`;
68
+ if (!options.bypassCache) {
69
+ const cached = cacheGet(cacheKey);
70
+ if (cached)
71
+ return cached;
72
+ }
73
+ const urls = [buildPrimaryImageUrl(mediaId, host), buildLegacyImageUrl(mediaId, host)];
74
+ let lastError;
75
+ for (const url of urls) {
76
+ try {
77
+ const data = await fetchFrom(url, options.signal);
78
+ if (imageDataHasRenderableSource(data)) {
79
+ cacheSet(cacheKey, data);
80
+ }
81
+ return data;
82
+ }
83
+ catch (err) {
84
+ if (err?.name === 'AbortError') {
85
+ throw err;
86
+ }
87
+ lastError = err;
88
+ }
89
+ }
90
+ if (lastError instanceof Error) {
91
+ throw lastError;
92
+ }
93
+ throw new Error(`Failed to fetch image data for mediaId ${mediaId}`);
94
+ }
@@ -0,0 +1,11 @@
1
+ import type { ImageData, ImageVariantsMap } from '../types.js';
2
+ export type FetchImageOptions = {
3
+ cdnHost?: string;
4
+ signal?: AbortSignal;
5
+ bypassCache?: boolean;
6
+ };
7
+ export declare const DEFAULT_CDN_HOST = "https://cdn.ing";
8
+ export declare function buildPlaceholderImageUrl(mediaId: number, cdnHost?: string): string;
9
+ export declare function imageVariantsHaveRenderableSource(variants?: ImageVariantsMap | null): boolean;
10
+ export declare function imageDataHasRenderableSource(data: ImageData): boolean;
11
+ export declare function fetchImageData(mediaId: number, options?: FetchImageOptions): Promise<ImageData>;
@@ -0,0 +1,94 @@
1
+ import { cacheGet, cacheSet } from './cache.js';
2
+ export const DEFAULT_CDN_HOST = 'https://cdn.ing';
3
+ function normalizeHost(cdnHost) {
4
+ return (cdnHost ?? DEFAULT_CDN_HOST).replace(/\/$/, '');
5
+ }
6
+ function buildPrimaryImageUrl(mediaId, cdnHost) {
7
+ const host = normalizeHost(cdnHost);
8
+ return `${host}/assets/images/${mediaId}`;
9
+ }
10
+ function buildLegacyImageUrl(mediaId, cdnHost) {
11
+ const host = normalizeHost(cdnHost);
12
+ return `${host}/i/r/${mediaId}`;
13
+ }
14
+ export function buildPlaceholderImageUrl(mediaId, cdnHost) {
15
+ const host = normalizeHost(cdnHost);
16
+ return `${host}/assets/low_res_thumb/${mediaId}`;
17
+ }
18
+ function hasRenderableUrlVariant(variant) {
19
+ if (!variant)
20
+ return false;
21
+ const candidates = [variant.sm, variant.md, variant.lg, variant.full];
22
+ return candidates.some((value) => typeof value === 'string' && value.trim().length > 0);
23
+ }
24
+ export function imageVariantsHaveRenderableSource(variants) {
25
+ if (!variants)
26
+ return false;
27
+ return ['AVIF', 'WEBP', 'JPEG'].some((format) => {
28
+ const entry = variants?.[format];
29
+ return hasRenderableUrlVariant(entry);
30
+ });
31
+ }
32
+ function hasUrlValue(value) {
33
+ return typeof value === 'string' && value.trim().length > 0;
34
+ }
35
+ export function imageDataHasRenderableSource(data) {
36
+ if (!data)
37
+ return false;
38
+ if (imageVariantsHaveRenderableSource(data.variants_data?.variants ?? null)) {
39
+ return true;
40
+ }
41
+ const raw = data;
42
+ const directFields = [
43
+ raw.img_url,
44
+ raw.file_data_url,
45
+ raw.file_data_thumbnail_url,
46
+ raw.img_src,
47
+ raw.med_src,
48
+ raw.thumb_src,
49
+ raw.low_res_thumb,
50
+ ];
51
+ return directFields.some(hasUrlValue);
52
+ }
53
+ async function fetchFrom(url, signal) {
54
+ const res = await fetch(url, { signal });
55
+ if (!res.ok) {
56
+ const error = new Error(`Failed to fetch image data (status ${res.status}) from ${url}`);
57
+ error.status = res.status;
58
+ throw error;
59
+ }
60
+ return (await res.json());
61
+ }
62
+ export async function fetchImageData(mediaId, options = {}) {
63
+ if (!Number.isFinite(mediaId)) {
64
+ throw new Error('Invalid mediaId provided to fetchImageData');
65
+ }
66
+ const host = normalizeHost(options.cdnHost);
67
+ const cacheKey = `image:${host}:${mediaId}`;
68
+ if (!options.bypassCache) {
69
+ const cached = cacheGet(cacheKey);
70
+ if (cached)
71
+ return cached;
72
+ }
73
+ const urls = [buildPrimaryImageUrl(mediaId, host), buildLegacyImageUrl(mediaId, host)];
74
+ let lastError;
75
+ for (const url of urls) {
76
+ try {
77
+ const data = await fetchFrom(url, options.signal);
78
+ if (imageDataHasRenderableSource(data)) {
79
+ cacheSet(cacheKey, data);
80
+ }
81
+ return data;
82
+ }
83
+ catch (err) {
84
+ if (err?.name === 'AbortError') {
85
+ throw err;
86
+ }
87
+ lastError = err;
88
+ }
89
+ }
90
+ if (lastError instanceof Error) {
91
+ throw lastError;
92
+ }
93
+ throw new Error(`Failed to fetch image data for mediaId ${mediaId}`);
94
+ }
@@ -0,0 +1,12 @@
1
+ // Lightweight multi-level cache inspired by ecosystem guidelines
2
+ // L1 (instance) will be handled in components via refs/state; this is L2 (module-level) cache
3
+ const l2Cache = new Map();
4
+ export function cacheGet(key) {
5
+ return l2Cache.get(key);
6
+ }
7
+ export function cacheSet(key, value) {
8
+ l2Cache.set(key, value);
9
+ }
10
+ export function cacheHas(key) {
11
+ return l2Cache.has(key);
12
+ }
@@ -0,0 +1,3 @@
1
+ export declare function cacheGet<T>(key: string): T | undefined;
2
+ export declare function cacheSet<T>(key: string, value: T): void;
3
+ export declare function cacheHas(key: string): boolean;
@@ -0,0 +1,12 @@
1
+ // Lightweight multi-level cache inspired by ecosystem guidelines
2
+ // L1 (instance) will be handled in components via refs/state; this is L2 (module-level) cache
3
+ const l2Cache = new Map();
4
+ export function cacheGet(key) {
5
+ return l2Cache.get(key);
6
+ }
7
+ export function cacheSet(key, value) {
8
+ l2Cache.set(key, value);
9
+ }
10
+ export function cacheHas(key) {
11
+ return l2Cache.has(key);
12
+ }
package/package.json ADDED
@@ -0,0 +1,108 @@
1
+ {
2
+ "name": "@page-speed/img",
3
+ "version": "0.0.1",
4
+ "description": "Performance-optimized React Image component. Drop-in image implementation of web.dev best practices with zero configuration.",
5
+ "keywords": [
6
+ "react",
7
+ "hooks",
8
+ "performance",
9
+ "web-vitals",
10
+ "core-web-vitals",
11
+ "LCP",
12
+ "CLS",
13
+ "INP",
14
+ "FCP",
15
+ "TTFB",
16
+ "page-speed",
17
+ "optimization",
18
+ "lazy-load",
19
+ "responsive-images",
20
+ "resource-hints",
21
+ "web.dev",
22
+ "lighthouse",
23
+ "pagespeed",
24
+ "seo",
25
+ "tree-shakeable"
26
+ ],
27
+ "homepage": "https://github.com/opensite-ai/page-speed-img#readme",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/opensite-ai/page-speed-img.git"
31
+ },
32
+ "bugs": {
33
+ "url": "https://github.com/opensite-ai/page-speed-img/issues"
34
+ },
35
+ "author": "OpenSite AI (https://opensite.ai)",
36
+ "license": "MIT",
37
+ "private": false,
38
+ "type": "module",
39
+ "main": "dist/index.cjs",
40
+ "module": "dist/index.js",
41
+ "types": "dist/index.d.ts",
42
+ "files": [
43
+ "dist"
44
+ ],
45
+ "sideEffects": false,
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "exports": {
50
+ ".": {
51
+ "import": "./dist/index.js",
52
+ "require": "./dist/index.cjs",
53
+ "types": "./dist/index.d.ts"
54
+ },
55
+ "./core": {
56
+ "import": "./dist/core/index.js",
57
+ "require": "./dist/core/index.cjs",
58
+ "types": "./dist/core/index.d.ts"
59
+ },
60
+ "./core/img": {
61
+ "import": "./dist/core/Img.js",
62
+ "require": "./dist/core/Img.cjs",
63
+ "types": "./dist/core/Img.d.ts"
64
+ }
65
+ },
66
+ "scripts": {
67
+ "build": "pnpm run build:lib && pnpm run build:umd",
68
+ "build:lib": "tsc && node scripts/emit-cjs.js",
69
+ "build:umd": "vite build --config vite.umd.config.ts",
70
+ "build:watch": "tsc -w",
71
+ "clean": "rimraf dist",
72
+ "lint": "eslint src --ext .ts,.tsx",
73
+ "test": "vitest run",
74
+ "bundle-analysis": "node scripts/analyze-bundle.js || true",
75
+ "prepare": "husky",
76
+ "prepack": "pnpm run build",
77
+ "prepublishOnly": "pnpm run build"
78
+ },
79
+ "peerDependencies": {
80
+ "react": ">=17.0.0",
81
+ "react-dom": ">=17.0.0"
82
+ },
83
+ "devDependencies": {
84
+ "@commitlint/cli": "^20.1.0",
85
+ "@commitlint/config-conventional": "^20.0.0",
86
+ "@types/react": "^18.3.3",
87
+ "@types/react-dom": "^18.3.0",
88
+ "@typescript-eslint/eslint-plugin": "^8.46.0",
89
+ "@typescript-eslint/parser": "^8.46.0",
90
+ "eslint": "^9.37.0",
91
+ "happy-dom": "^15.11.7",
92
+ "husky": "^9.1.7",
93
+ "typescript": "^5.6.2",
94
+ "vite": "^5.4.20",
95
+ "@vitejs/plugin-react": "^4.2.1",
96
+ "terser": "^5.44.0",
97
+ "vitest": "^3.2.4",
98
+ "@types/node": "^20.17.6"
99
+ },
100
+ "dependencies": {
101
+ "@page-speed/hooks": "0.1.3"
102
+ },
103
+ "packageManager": "pnpm@10.24.0",
104
+ "engines": {
105
+ "node": ">=18.0.0",
106
+ "pnpm": ">=9.0.0"
107
+ }
108
+ }