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.
- package/README.md +58 -38
- package/dist/index.js +1 -1
- 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`
|
|
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
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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
|
-
|
|
67
|
-
|
|
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
|
|
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()})});})();
|