shopic 0.0.2 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +58 -38
  2. package/dist/index.js +1 -1
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -16,55 +16,75 @@ is public even though this repository is not — that is the price of a free CDN
16
16
 
17
17
  ```bash
18
18
  pnpm install
19
- pnpm dev
19
+ pnpm dev # watch + rebuild; also serves dist/ on http://localhost:3000
20
+ pnpm check # tsc
21
+ pnpm lint # eslint + prettier
20
22
  ```
21
23
 
22
- `pnpm dev` serves the bundle at `http://localhost:3000` with live reload.
24
+ `pnpm dev` is for building and type-checking while you work. It is **not** a way
25
+ to preview against the live site — see below.
23
26
 
24
- Put this in **Site Settings Custom Code Footer** once, and leave it there. It
25
- loads the CDN build for everyone, and the local build only for a browser you have
26
- flipped into dev mode:
27
+ The site loads the bundle from jsDelivr. One line in
28
+ **Site Settings Custom Code Footer**, and that is the whole integration:
27
29
 
28
30
  ```html
29
- <script>
30
- (() => {
31
- const PROD = 'https://cdn.jsdelivr.net/npm/shopic@0.0.2/dist/index.js';
32
- const DEV = 'https://localhost:3000/index.js';
33
-
34
- const load = (src, fallback) => {
35
- const s = document.createElement('script');
36
- s.src = src;
37
- if (fallback) s.onerror = fallback;
38
- document.head.appendChild(s);
39
- };
40
-
41
- if (localStorage.getItem('shopic-dev') === 'on') load(DEV, () => load(PROD));
42
- else load(PROD);
43
- })();
44
- </script>
31
+ <script src="https://cdn.jsdelivr.net/npm/shopic@0.0.2/dist/index.js"></script>
45
32
  ```
46
33
 
47
- Flip it on in the console on the staging domain, once:
34
+ GSAP is **not** loaded there. It comes from Webflow's native Site Settings
35
+ toggle (3.15.0 with SplitText); a second copy would register ScrollTrigger twice.
48
36
 
49
- ```js
50
- localStorage.setItem('shopic-dev', 'on'); // off: localStorage.removeItem('shopic-dev')
51
- ```
52
-
53
- Without that gate every visitor's browser would probe your localhost. With it,
54
- only yours does, and it falls back to the CDN the moment `pnpm dev` is not running.
37
+ ## Seeing a change on the site
55
38
 
56
- **The dev server must speak HTTPS.** Measured 2026-09-01 on the staging domain:
57
- Chrome blocks `http://localhost:3000/index.js` as mixed content when the page
58
- itself is HTTPS. The script tag appears in the DOM, the request never completes,
59
- and nothing logs — it looks like the bundle loaded and silently did nothing.
60
- `http://localhost` is a *potentially trustworthy origin*, which is a different
61
- rule from mixed-content subresource blocking; only the first one exempts it.
39
+ Publish it. There is no localhost preview.
62
40
 
63
- Serve it over TLS instead (`mkcert localhost` once, then point esbuild's `serve`
64
- at the key and cert), and use `https://localhost:3000/index.js` in the loader.
41
+ ```bash
42
+ pnpm check && pnpm lint && pnpm build
43
+ npm version patch --no-git-tag-version
44
+ git add -A && git commit -m "…" && git push
45
+ npm publish # needs the 2FA passkey
46
+ ```
65
47
 
66
- The dev URL has no `/dist` in it, because esbuild serves that directory as the
67
- root (`src/pages/home.ts` `/pages/home.js`).
48
+ Then bump the version in the footer snippet and reload the site. jsDelivr serves
49
+ a new version within a minute or so.
50
+
51
+ ### Why there is no localhost preview
52
+
53
+ The obvious setup — footer script pointing at `http://localhost:3000` behind a
54
+ localStorage flag — does not work from a published Webflow page, and cannot be
55
+ made to work without dismantling browser protections. Established by measurement
56
+ on 2026-09-01, in this order:
57
+
58
+ 1. **Mixed content.** An http subresource on an https page is blocked. Silently:
59
+ the script tag is in the DOM, the request never completes, nothing is logged.
60
+ It looks exactly like a bundle that loaded and did nothing.
61
+ 2. **Private Network Access.** Clearing the first one only reveals this:
62
+ `Permission was denied for this request to access the loopback address space`.
63
+ esbuild's server cannot opt in — it answers OPTIONS with a 404 and never sends
64
+ `Access-Control-Allow-Private-Network`.
65
+ 3. **Local Network Access.** Sending those headers from a proxy is still not
66
+ enough. Chrome now requires the *user* to grant permission before any site
67
+ reaches a loopback address, and a `<script src>` cannot request it —
68
+ `targetAddressSpace` is a `fetch()` option, so it would mean fetching the code
69
+ and injecting it by hand.
70
+
71
+ Each of those exists to stop a public page from reaching services on the
72
+ visitor's machine. Getting a preview would mean a browser flag, plus a proxy,
73
+ plus a granted permission — three mitigations bypassed so that a reload is
74
+ faster.
75
+
76
+ Two other routes were tried and rejected:
77
+
78
+ - **A local TLS certificate (`mkcert`).** Solves step 1, but `mkcert -install`
79
+ puts a private CA in the system trust store, and whoever reads that CA's key
80
+ can mint a certificate for *any* domain that the browser accepts without
81
+ warning. Permanent, machine-wide exposure for a development convenience.
82
+ - **A Chrome flag on a throwaway profile.** Also solves step 1, but stops at
83
+ step 3.
84
+
85
+ **Do not reopen this.** If a preview loop becomes genuinely necessary, the answer
86
+ is a local page that loads the bundle over plain http from a plain http page —
87
+ not a way to make the published site reach this machine.
68
88
 
69
89
  ## Releasing
70
90
 
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- "use strict";(()=>{var l=new Map,p=(t,r)=>{if(l.has(t))throw new Error(`[shopic] Animation "${t}" is registered twice.`);l.set(t,r)},m=(t,r=document)=>{let s=r.querySelectorAll("[data-anim]");for(let e of s){let i=e.dataset.anim;if(!i||e.dataset.animState==="ready")continue;let o=l.get(i);if(!o){console.warn(`[shopic] No animation registered for data-anim="${i}".`,e);continue}e.dataset.animState="ready",o(e,t)}};var g="split_line",w="split_word",f="split_word-inner",E=t=>{let r=document.createDocumentFragment(),s=[],e=document.createElement("span");e.className=g,r.appendChild(e);let i=(n,c)=>{if(!n)return;let a=document.createElement("span");a.className=w;let d=document.createElement("span");d.className=c?`${f} ${c}`:f,d.textContent=n,a.appendChild(d),e.appendChild(a),s.push(d)},o=(n,c)=>{for(let a of n.split(/(\s+)/))a&&(/^\s+$/.test(a)?e.appendChild(document.createTextNode(" ")):i(a,c))};for(let n of Array.from(t.childNodes))n.nodeType===Node.TEXT_NODE?o(n.textContent??""):n instanceof HTMLBRElement?(e=document.createElement("span"),e.className=g,r.appendChild(e)):n instanceof HTMLElement&&o(n.textContent??"",n.className||void 0);return t.textContent="",t.appendChild(r),s},u={yPercent:0,duration:.9,ease:"power4.out",stagger:.035};p("headline",(t,{gsap:r,ScrollTrigger:s,reducedMotion:e})=>{if(e){t.style.visibility="visible";return}let i=E(t);if(r.set(i,{yPercent:110}),t.style.visibility="visible",t.dataset.animTrigger==="load"||!s){r.to(i,{...u,delay:.3});return}r.to(i,{...u,scrollTrigger:{trigger:t,start:"top 85%"}})});var h=26;p("marquee",(t,{gsap:r,reducedMotion:s})=>{if(s)return;let e=Number(t.dataset.animSets??4),i=Number(t.dataset.animDuration??h);if(!Number.isFinite(e)||e<2)return;let o=()=>t.scrollWidth/e,n=r.to(t,{x:()=>-o(),duration:i,ease:"none",repeat:-1,modifiers:{x:a=>`${Number.parseFloat(a)%o()}px`}}),c=t.parentElement;c&&(c.addEventListener("pointerenter",()=>n.pause()),c.addEventListener("pointerleave",()=>n.resume()))});var y="is-visible";p("reveal",(t,{reducedMotion:r})=>{let s=Number(t.dataset.animDelay??0);if(s&&(t.style.transitionDelay=`${s}ms`),r||!("IntersectionObserver"in window)){t.classList.add(y);return}let e=new IntersectionObserver(i=>{for(let o of i)o.isIntersecting&&(o.target.classList.add(y),e.unobserve(o.target))},{threshold:.15});e.observe(t)});var T=()=>{let{gsap:t,ScrollTrigger:r}=window;if(!t)throw new Error("[shopic] GSAP is not on the page. Enable it in Webflow \u2192 Site Settings \u2192 Custom Code \u2192 GSAP.");return r&&t.registerPlugin(r),{gsap:t,ScrollTrigger:r}},S=()=>window.matchMedia("(prefers-reduced-motion: reduce)").matches;window.Webflow||(window.Webflow=[]);window.Webflow.push(()=>{let{gsap:t,ScrollTrigger:r}=T();m({gsap:t,ScrollTrigger:r,reducedMotion:S()})});})();
1
+ "use strict";(()=>{var M=new Map,f=(e,n)=>{if(M.has(e))throw new Error(`[shopic] Animation "${e}" is registered twice.`);M.set(e,n)},C=(e,n=document)=>{let r=n.querySelectorAll("[data-anim]");for(let t of r){let l=t.dataset.anim;if(!l||t.dataset.animState==="ready")continue;let a=M.get(l);if(!a){console.warn(`[shopic] No animation registered for data-anim="${l}".`,t);continue}t.dataset.animState="ready",a(t,e)}};var $=2,F=150,U=[0,.25,.5,.6,.7,.8,.9,1],x="shopic:slide-enter";f("carousel",(e,{reducedMotion:n})=>{let r=Array.from(e.querySelectorAll(":scope > [data-slide]"));if(r.length<2)return;let t=e.closest("section")??e.parentElement??document.body,l=Array.from(t.querySelectorAll("[data-tab]")),a=n?"auto":"smooth",s=o=>o.getBoundingClientRect().left-e.getBoundingClientRect().left+e.scrollLeft,d=()=>parseFloat(getComputedStyle(e).paddingLeft)||0,i=(o,c)=>{e.scrollTo({left:s(o)-d(),behavior:c?a:"auto"})},m=r[r.length-1].cloneNode(!0),g=r[0].cloneNode(!0);for(let o of[m,g]){o.dataset.slideClone="true",o.setAttribute("aria-hidden","true");for(let c of o.querySelectorAll("[data-anim]"))c.dataset.animState="ready"}e.insertBefore(m,r[0]),e.append(g);let L=Array.from(e.querySelectorAll(":scope > [data-slide]")),A=o=>{for(let c of l)c.classList.toggle("is-active",c.dataset.tab===o)};for(let o of l)o.addEventListener("click",c=>{c.preventDefault();let T=o.dataset.tab,y=r.find(u=>u.dataset.slide===T);y&&i(y,!0)});let S=new Map(L.map(o=>[o,0])),p=null,b=new IntersectionObserver(o=>{for(let u of o)S.set(u.target,u.isIntersecting?u.intersectionRatio:0);let c=null,T=0;for(let u of L){let N=S.get(u)??0;N>T&&(T=N,c=u)}if(!c)return;let y=c.dataset.slide;y&&A(y);for(let u of L)u.classList.toggle("is-current",u===c);c!==p&&(p=c,c.dispatchEvent(new CustomEvent(x)))},{root:e,threshold:U});for(let o of L)b.observe(o);e.scrollLeft=s(r[0])-d();let h=()=>{let o=null,c=1/0;for(let T of L){let y=Math.abs(s(T)-d()-e.scrollLeft);y<c&&(c=y,o=T)}return o},w=()=>{let o=h();if(!o?.dataset.slideClone)return;let c=r.find(T=>T.dataset.slide===o.dataset.slide);c&&(e.style.scrollSnapType="none",e.scrollLeft-=s(o)-s(c),e.offsetHeight,e.style.scrollSnapType="")};if("onscrollend"in window)e.addEventListener("scrollend",w);else{let o;e.addEventListener("scroll",()=>{clearTimeout(o),o=setTimeout(w,F)})}let E=!1,H=0,I=0;e.addEventListener("pointerdown",o=>{E=!0,e.classList.add("is-dragging"),H=o.clientX,I=e.scrollLeft}),window.addEventListener("pointermove",o=>{E&&(e.scrollLeft=I-(o.clientX-H)*$)}),window.addEventListener("pointerup",()=>{if(!E)return;E=!1,e.classList.remove("is-dragging");let o=h();o&&i(o,!0)})});var R="!<>-_\\/[]{}=+*^?#$%01",O=new WeakMap,v=(e,{duration:n=1050,stagger:r=.75,text:t,reducedMotion:l=!1}={})=>{if(l){t!==void 0&&(e.textContent=t);return}if(t!==void 0)e.dataset.scrambleText=t;else if(!e.dataset.scrambleText){let p=e.textContent;if(!p||!p.trim())return;e.dataset.scrambleText=p}let a=e.dataset.scrambleText;if(!a||!a.trim())return;let s=Array.from(a),d=s.length;e.textContent="";let i=document.createDocumentFragment(),m=s.map(p=>{let b=document.createElement("span");return b.textContent=p,i.append(b),b});e.append(i);let g=Symbol("scramble");O.set(e,g);let L=s.map((p,b)=>d<=1?0:b/(d-1)*n*r),A=performance.now(),S=p=>{if(O.get(e)!==g)return;let b=p-A,h=!0;s.forEach((w,E)=>{w!==" "&&(b>=L[E]?m[E].textContent=w:(h=!1,m[E].textContent=R[Math.random()*R.length|0]))}),h||requestAnimationFrame(S)};requestAnimationFrame(S)},_=(e,n={})=>{e.dataset.scrambleHoverBound||(e.dataset.scrambleHoverBound="true",e.addEventListener("pointerenter",()=>v(e,n)))};var X=.5,V="power2.out",j=".status-pill, .evidence_label, .evidence_sku";f("evidence",(e,{gsap:n,reducedMotion:r})=>{let t=Array.from(e.children);if(!t.length)return;if(r){for(let i of t)i.style.height="auto",i.style.opacity="1";return}let l=()=>{n.killTweensOf(t),n.set(t,{height:0,opacity:0,overflow:"hidden"});let i=n.timeline();for(let m of t)i.call(()=>{for(let g of m.querySelectorAll(j))v(g,{duration:630,stagger:.6})}),i.to(m,{height:"auto",opacity:1,duration:X,ease:V,onComplete:()=>n.set(m,{height:"auto"})})},a=!1,s=!1,d=()=>{if(!a)return;let i=e.closest("[data-slide]");i&&!i.classList.contains("is-current")||(s=!0,l())};if("IntersectionObserver"in window){let i=new IntersectionObserver(m=>{for(let g of m)g.isIntersecting&&(a=!0,d(),s&&i.disconnect())},{threshold:.2});i.observe(e)}else a=!0,d();e.closest("[data-slide]")?.addEventListener(x,d)});var D="split_line",z="split_word",G="split_word-inner",J=e=>{let n=document.createDocumentFragment(),r=[],t=document.createElement("span");t.className=D,n.appendChild(t);let l=(s,d)=>{if(!s)return;let i=document.createElement("span");i.className=z;let m=document.createElement("span");m.className=d?`${G} ${d}`:G,m.textContent=s,i.appendChild(m),t.appendChild(i),r.push(m)},a=(s,d)=>{for(let i of s.split(/(\s+)/))i&&(/^\s+$/.test(i)?t.appendChild(document.createTextNode(" ")):l(i,d))};for(let s of Array.from(e.childNodes))s.nodeType===Node.TEXT_NODE?a(s.textContent??""):s instanceof HTMLBRElement?(t=document.createElement("span"),t.className=D,n.appendChild(t)):s instanceof HTMLElement&&a(s.textContent??"",s.className||void 0);return e.textContent="",e.appendChild(n),r},q={yPercent:0,duration:.9,ease:"power4.out",stagger:.035};f("headline",(e,{gsap:n,ScrollTrigger:r,reducedMotion:t})=>{if(t){e.style.visibility="visible";return}let l=J(e);if(n.set(l,{yPercent:110}),e.style.visibility="visible",e.dataset.animTrigger==="load"||!r){n.to(l,{...q,delay:.3});return}n.to(l,{...q,scrollTrigger:{trigger:e,start:"top 85%"}})});var K=26;f("marquee",(e,{gsap:n,reducedMotion:r})=>{if(r)return;let t=Number(e.dataset.animSets??4),l=Number(e.dataset.animDuration??K);if(!Number.isFinite(t)||t<2)return;let a=()=>e.scrollWidth/t,s=n.to(e,{x:()=>-a(),duration:l,ease:"none",repeat:-1,modifiers:{x:i=>`${Number.parseFloat(i)%a()}px`}}),d=e.parentElement;d&&(d.addEventListener("pointerenter",()=>s.pause()),d.addEventListener("pointerleave",()=>s.resume()))});var B="is-visible";f("reveal",(e,{reducedMotion:n})=>{let r=Number(e.dataset.animDelay??0);if(r&&(e.style.transitionDelay=`${r}ms`),n||!("IntersectionObserver"in window)){e.classList.add(B);return}let t=new IntersectionObserver(l=>{for(let a of l)a.isIntersecting&&(a.target.classList.add(B),t.unobserve(a.target))},{threshold:.15});t.observe(e)});f("scramble",(e,{reducedMotion:n})=>{let r={duration:Number(e.dataset.animDuration??1050),stagger:Number(e.dataset.animStagger??.75),reducedMotion:n};if(n||!("IntersectionObserver"in window)){v(e,r);return}let t=new IntersectionObserver(l=>{for(let a of l)a.isIntersecting&&(v(e,r),_(e,r),t.unobserve(a.target))},{threshold:.2});t.observe(e)});var P=()=>{let{gsap:e,ScrollTrigger:n}=window;if(!e)throw new Error("[shopic] GSAP is not on the page. Enable it in Webflow \u2192 Site Settings \u2192 Custom Code \u2192 GSAP.");return n&&e.registerPlugin(n),{gsap:e,ScrollTrigger:n}},W=()=>window.matchMedia("(prefers-reduced-motion: reduce)").matches;window.Webflow||(window.Webflow=[]);window.Webflow.push(()=>{let{gsap:e,ScrollTrigger:n}=P();C({gsap:e,ScrollTrigger:n,reducedMotion:W()})});})();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shopic",
3
- "version": "0.0.2",
3
+ "version": "0.1.0",
4
4
  "description": "Custom code bundle for the Shopic Webflow site.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,