shopic 0.0.1 → 0.0.2
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 +14 -5
- package/dist/index.js +1 -1
- package/dist/index.js.map +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,8 +28,8 @@ flipped into dev mode:
|
|
|
28
28
|
```html
|
|
29
29
|
<script>
|
|
30
30
|
(() => {
|
|
31
|
-
const PROD = 'https://cdn.jsdelivr.net/npm/shopic@0.0.
|
|
32
|
-
const DEV = '
|
|
31
|
+
const PROD = 'https://cdn.jsdelivr.net/npm/shopic@0.0.2/dist/index.js';
|
|
32
|
+
const DEV = 'https://localhost:3000/index.js';
|
|
33
33
|
|
|
34
34
|
const load = (src, fallback) => {
|
|
35
35
|
const s = document.createElement('script');
|
|
@@ -53,9 +53,18 @@ localStorage.setItem('shopic-dev', 'on'); // off: localStorage.removeItem('sho
|
|
|
53
53
|
Without that gate every visitor's browser would probe your localhost. With it,
|
|
54
54
|
only yours does, and it falls back to the CDN the moment `pnpm dev` is not running.
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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.
|
|
62
|
+
|
|
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.
|
|
65
|
+
|
|
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`).
|
|
59
68
|
|
|
60
69
|
## Releasing
|
|
61
70
|
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";(()=>{var
|
|
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()})});})();
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../bin/live-reload.js", "../src/anim/registry.ts", "../src/anim/headline.ts", "../src/anim/marquee.ts", "../src/anim/reveal.ts", "../src/lib/gsap.ts", "../src/index.ts"],
|
|
4
|
+
"sourcesContent": ["new EventSource(`${SERVE_ORIGIN}/esbuild`).addEventListener('change', () => location.reload());\n", "import type { Gsap, ScrollTriggerType } from '$lib/gsap';\n\nexport type AnimationContext = {\n gsap: Gsap;\n ScrollTrigger?: ScrollTriggerType;\n reducedMotion: boolean;\n};\n\nexport type AnimationInit = (element: HTMLElement, context: AnimationContext) => void;\n\nconst registry = new Map<string, AnimationInit>();\n\n/**\n * Registers an animation under the name used in `data-anim`.\n * The client edits content in the Designer; the attribute is the only contract\n * between their markup and this bundle.\n */\nexport const registerAnimation = (name: string, init: AnimationInit): void => {\n if (registry.has(name)) {\n throw new Error(`[shopic] Animation \"${name}\" is registered twice.`);\n }\n\n registry.set(name, init);\n};\n\n/**\n * Runs every registered animation against the elements that ask for it.\n * Unknown names warn rather than throw \u2014 a stray attribute in the Designer\n * must not take the whole bundle down.\n *\n * Initialisation is once per element, and the marker lives on the element\n * rather than in module scope so that a *second copy* of this bundle sees it\n * too. That is not hypothetical: a stale inlined copy alongside the CDN one\n * re-split an already-split headline, and because the splitter carries a\n * parent's className onto the words it produces, every word inherited\n * `split_line` (display: block) and stacked one per line.\n */\nexport const runAnimations = (context: AnimationContext, root: ParentNode = document): void => {\n const elements = root.querySelectorAll<HTMLElement>('[data-anim]');\n\n for (const element of elements) {\n const name = element.dataset.anim;\n if (!name) continue;\n if (element.dataset.animState === 'ready') continue;\n\n const init = registry.get(name);\n\n if (!init) {\n // eslint-disable-next-line no-console\n console.warn(`[shopic] No animation registered for data-anim=\"${name}\".`, element);\n continue;\n }\n\n element.dataset.animState = 'ready';\n init(element, context);\n }\n};\n", "import { registerAnimation } from './registry';\n\nconst LINE = 'split_line';\nconst WORD = 'split_word';\nconst INNER = 'split_word-inner';\n\n/**\n * Wraps every word in an overflow-hidden span so it can slide up into view.\n * `<br>` starts a new line; existing child spans (accent colours) survive as\n * single words and keep their class. The class names are real Webflow styles \u2014\n * a class that only ever exists at runtime gets stripped from the published CSS.\n */\nconst splitHeadline = (element: HTMLElement): HTMLElement[] => {\n const fragment = document.createDocumentFragment();\n const words: HTMLElement[] = [];\n let line = document.createElement('span');\n line.className = LINE;\n fragment.appendChild(line);\n\n const addWord = (text: string, className?: string): void => {\n if (!text) return;\n\n const outer = document.createElement('span');\n outer.className = WORD;\n\n const inner = document.createElement('span');\n inner.className = className ? `${INNER} ${className}` : INNER;\n inner.textContent = text;\n\n outer.appendChild(inner);\n line.appendChild(outer);\n words.push(inner);\n };\n\n const addChunks = (text: string, className?: string): void => {\n for (const chunk of text.split(/(\\s+)/)) {\n if (!chunk) continue;\n if (/^\\s+$/.test(chunk)) line.appendChild(document.createTextNode(' '));\n else addWord(chunk, className);\n }\n };\n\n for (const node of Array.from(element.childNodes)) {\n if (node.nodeType === Node.TEXT_NODE) {\n addChunks(node.textContent ?? '');\n } else if (node instanceof HTMLBRElement) {\n line = document.createElement('span');\n line.className = LINE;\n fragment.appendChild(line);\n } else if (node instanceof HTMLElement) {\n addChunks(node.textContent ?? '', node.className || undefined);\n }\n }\n\n element.textContent = '';\n element.appendChild(fragment);\n\n return words;\n};\n\nconst TWEEN = { yPercent: 0, duration: 0.9, ease: 'power4.out', stagger: 0.035 } as const;\n\nregisterAnimation('headline', (element, { gsap, ScrollTrigger, reducedMotion }) => {\n if (reducedMotion) {\n element.style.visibility = 'visible';\n return;\n }\n\n const words = splitHeadline(element);\n gsap.set(words, { yPercent: 110 });\n element.style.visibility = 'visible';\n\n // The hero headline is above the fold, so it plays on load; everything else\n // waits for the scroll to reach it.\n if (element.dataset.animTrigger === 'load' || !ScrollTrigger) {\n gsap.to(words, { ...TWEEN, delay: 0.3 });\n return;\n }\n\n gsap.to(words, { ...TWEEN, scrollTrigger: { trigger: element, start: 'top 85%' } });\n});\n", "import { registerAnimation } from './registry';\n\nconst DEFAULT_DURATION = 26;\n\n/**\n * Infinite logo strip. The track holds the same set of logos repeated N times,\n * so translating by exactly one set's width lands on an identical frame \u2014 no\n * measuring loop, no drift. N comes from the DOM, not a constant, so adding a\n * logo in the Designer cannot desync the loop.\n */\nregisterAnimation('marquee', (element, { gsap, reducedMotion }) => {\n if (reducedMotion) return;\n\n const sets = Number(element.dataset.animSets ?? 4);\n const duration = Number(element.dataset.animDuration ?? DEFAULT_DURATION);\n if (!Number.isFinite(sets) || sets < 2) return;\n\n const distance = () => element.scrollWidth / sets;\n\n const tween = gsap.to(element, {\n x: () => -distance(),\n duration,\n ease: 'none',\n repeat: -1,\n modifiers: {\n // wrap inside one set so the tween never accumulates a huge offset\n x: (value: string) => `${Number.parseFloat(value) % distance()}px`,\n },\n });\n\n const parent = element.parentElement;\n if (!parent) return;\n\n parent.addEventListener('pointerenter', () => tween.pause());\n parent.addEventListener('pointerleave', () => tween.resume());\n});\n", "import { registerAnimation } from './registry';\n\nconst VISIBLE = 'is-visible';\n\n/**\n * Fade-and-rise on entry. The motion itself is a CSS transition on\n * `.hero_media`; this only decides when to add the state class, so the\n * client can restyle the easing in the Designer without touching the bundle.\n */\nregisterAnimation('reveal', (element, { reducedMotion }) => {\n const delay = Number(element.dataset.animDelay ?? 0);\n if (delay) element.style.transitionDelay = `${delay}ms`;\n\n if (reducedMotion || !('IntersectionObserver' in window)) {\n element.classList.add(VISIBLE);\n return;\n }\n\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (!entry.isIntersecting) continue;\n entry.target.classList.add(VISIBLE);\n observer.unobserve(entry.target);\n }\n },\n { threshold: 0.15 }\n );\n\n observer.observe(element);\n});\n", "import type { gsap as GsapStatic } from 'gsap';\nimport type { ScrollTrigger as ScrollTriggerStatic } from 'gsap/ScrollTrigger';\n\ndeclare global {\n interface Window {\n gsap?: typeof GsapStatic;\n ScrollTrigger?: typeof ScrollTriggerStatic;\n }\n}\n\nexport type Gsap = typeof GsapStatic;\nexport type ScrollTriggerType = typeof ScrollTriggerStatic;\n\n/**\n * GSAP ships from Webflow's native Site Settings toggle, not from this bundle.\n * A site transfer wipes Site Settings, so the toggle can silently disappear and\n * take every animation with it. Fail loudly here instead.\n */\nexport const requireGsap = (): { gsap: Gsap; ScrollTrigger?: ScrollTriggerType } => {\n const { gsap, ScrollTrigger } = window;\n\n if (!gsap) {\n throw new Error(\n '[shopic] GSAP is not on the page. Enable it in Webflow \u2192 Site Settings \u2192 Custom Code \u2192 GSAP.'\n );\n }\n\n if (ScrollTrigger) gsap.registerPlugin(ScrollTrigger);\n\n return { gsap, ScrollTrigger };\n};\n\n/**\n * Whether the visitor asked the OS to cut down on motion.\n */\nexport const prefersReducedMotion = (): boolean =>\n window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n", "import { runAnimations } from '$anim/index';\nimport { prefersReducedMotion, requireGsap } from '$lib/gsap';\n\nwindow.Webflow ||= [];\nwindow.Webflow.push(() => {\n const { gsap, ScrollTrigger } = requireGsap();\n\n runAnimations({ gsap, ScrollTrigger, reducedMotion: prefersReducedMotion() });\n});\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|