@ocarignan/vitrine 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 ADDED
@@ -0,0 +1,215 @@
1
+ # Vitrine
2
+
3
+ A draggable horizontal slider with a shared-element zoom into a
4
+ fullscreen lightbox. Extracted as a self-contained, prop-driven component so it
5
+ can drop into any React project — no host grid system required.
6
+
7
+ Built with React 19, [`motion`](https://motion.dev), and the native
8
+ [View Transitions API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API)
9
+ (progressive enhancement — falls back to a plain open/close where unsupported).
10
+ The caption morphs between projects by default, powered by
11
+ [`@ocarignan/metamorphosis`](https://www.npmjs.com/package/@ocarignan/metamorphosis) —
12
+ a dependency-free animated-text component that comes along as a dependency. Pass
13
+ the bundled `PlainCaption` (or your own component) to the `Caption` prop to opt
14
+ out of the animation.
15
+
16
+ ## Repo layout
17
+
18
+ This repo is both the published library and a live demo:
19
+
20
+ - **`src/`** — the library. `Slider`, `Lightbox`, and the stylesheets. Built to
21
+ `dist/` with [`tsup`](https://tsup.egoist.dev) (ESM + CJS) and published to npm
22
+ as [`@ocarignan/vitrine`](https://www.npmjs.com/package/@ocarignan/vitrine).
23
+ - **`demo/`** — a standalone Vite app that imports the library from `../src` and
24
+ doubles as a smoke test of the public entry. It has its own `package.json`.
25
+
26
+ ## Features
27
+
28
+ - Drag (with inertia + snap), wheel/trackpad scroll, and click-to-center.
29
+ - Click to zoom into a fullscreen, swipeable lightbox.
30
+ - Shared-element view transition between the panel and the lightbox image.
31
+ - Optional looping muted **video** per panel, autoplaying only while active.
32
+ - Progressive hi-res image swap in the lightbox (low-res placeholder → hi-res).
33
+ - Mobile-tuned: centered snap, depth scaling, and drag-down-to-dismiss the
34
+ lightbox.
35
+ - Optional prev / close / next control bar in the lightbox (`lightboxControls`),
36
+ off by default.
37
+ - Optional morphing icon cursor (`morphCursor`) that follows the pointer and
38
+ animates between `+` (slider panel), `×` (active lightbox item / overlay) and
39
+ `←` / `→` (previous / next items), off by default.
40
+ - Keyboard navigation: `←` / `→` move the slider while it's focused or hovered,
41
+ and drive the lightbox (`←` / `→` / `Esc`) while it's open.
42
+
43
+ ## Run the demo
44
+
45
+ ```bash
46
+ cd demo
47
+ pnpm install # or npm install
48
+ pnpm dev # then open the printed localhost URL
49
+ ```
50
+
51
+ The demo (`demo/src/App.jsx`, `demo/src/demo-data.js`) renders the slider with
52
+ random artworks pulled live from the
53
+ [Art Institute of Chicago API](https://api.artic.edu/docs/) — it searches for
54
+ public-domain, image-bearing works under a random term each load, so you get a
55
+ different set every refresh. A second slider below it (`demo/src/video-data.js`)
56
+ drives looping `<video>` panels from the
57
+ [Pexels Video API](https://www.pexels.com/api/) — free, no-attribution MP4 stock
58
+ clips — showing the same component handling video items. That slider needs a free
59
+ Pexels key in `VITE_PEXELS_KEY` (copy `demo/.env.example` to `demo/.env.local`);
60
+ without it, the video section is simply skipped.
61
+
62
+ ## Install
63
+
64
+ ```bash
65
+ pnpm add @ocarignan/vitrine
66
+ # or: npm i @ocarignan/vitrine · bun add @ocarignan/vitrine · yarn add @ocarignan/vitrine
67
+ ```
68
+
69
+ `react` and `react-dom` (`>=18`) are peer dependencies; `motion` and
70
+ [`@ocarignan/metamorphosis`](https://www.npmjs.com/package/@ocarignan/metamorphosis)
71
+ (the morphing caption) come along as dependencies — both ship prebuilt, so
72
+ there's nothing to allowlist or build.
73
+
74
+ Import the component and its stylesheet once, then render with your items:
75
+
76
+ ```jsx
77
+ import { Slider } from "@ocarignan/vitrine";
78
+ import "@ocarignan/vitrine/styles.css";
79
+
80
+ <Slider items={items} />;
81
+ ```
82
+
83
+ The stylesheet is self-contained — the custom zoom cursors are inlined as data
84
+ URIs, so there are no asset files to host. Opt into a morphing icon cursor with
85
+ the `morphCursor` prop (see below).
86
+
87
+ ### Plain (non-animated) caption
88
+
89
+ The meta caption morphs between projects by default (letter morphing via
90
+ `metamorphosis`). To render it as plain `<h3>` / `<p>` instead, pass the bundled
91
+ `PlainCaption` to the `Caption` prop:
92
+
93
+ ```jsx
94
+ import { Slider, PlainCaption } from "@ocarignan/vitrine";
95
+
96
+ <Slider items={items} Caption={PlainCaption} />;
97
+ ```
98
+
99
+ Any component with the `({ as, children })` contract works as a `Caption` —
100
+ bring your own.
101
+
102
+ ### Morphing icon cursor
103
+
104
+ By default the slider uses static inline-SVG cursors (`+` to zoom in, `×` to
105
+ close). Pass `morphCursor` to replace them with a single cursor that follows the
106
+ pointer and **morphs** between icons depending on what's under it — powered by
107
+ `metamorphosis`'s `IconMorph`:
108
+
109
+ ```jsx
110
+ <Slider items={items} morphCursor />;
111
+ ```
112
+
113
+ | Context | Hovered target | Icon |
114
+ | --------- | ---------------------------------- | ------------ |
115
+ | Slider | a panel | `+` |
116
+ | Lightbox | the active (centered) item | `×` |
117
+ | Lightbox | the overlay (backdrop / empty gaps)| `×` |
118
+ | Lightbox | the previous item | `←` |
119
+ | Lightbox | the next item | `→` |
120
+
121
+ The cursor is white with a dark outline for contrast on any background. It only
122
+ activates on precise pointers (`pointer: fine`); touch devices, no-JS, and
123
+ `prefers-reduced-motion` fall back to the static cursors (the latter swaps icons
124
+ instantly instead of morphing). No extra setup — `IconMorph` ships with
125
+ `metamorphosis`, which is already a dependency.
126
+
127
+ ### Required CSS tokens
128
+
129
+ The stylesheets read a few CSS custom properties — define them on `:root` (see
130
+ `demo/src/demo.css`):
131
+
132
+ | Token | Used for |
133
+ | ----------------- | ------------------------------------------- |
134
+ | `--gap` | gap between slider panels (desktop) |
135
+ | `--accent-color` | meta title color, focus ring, control-bar background (when enabled) |
136
+ | `--text-color` | meta subtitle color |
137
+ | `--color-text` | control-bar icon color — close + arrows (when enabled) |
138
+
139
+ To get the rest of the page to cross-fade during the zoom, also set
140
+ `view-transition-name: root` on `:root`.
141
+
142
+ ## `<Slider>` props
143
+
144
+ | Prop | Type | Default | Description |
145
+ | ------------------- | -------- | ---------------------------------- | ------------------------------------------------------------------ |
146
+ | `items` | `Item[]` | — | The panels (see shape below). |
147
+ | `contentWidth` | `number` | `628` | Desktop width (px) of the active panel's content column. |
148
+ | `gap` | `number` | `32` | Desktop gap (px) between panels. |
149
+ | `columns` | `number` | `4` | Notional grid columns — only used to align the meta text. |
150
+ | `metaOffsetColumns` | `number` | `0` | Shift the meta text right by N columns on desktop (0 = flush). |
151
+ | `sideMargin` | `number` | `24` | Minimum viewport margin (px/side) the content column keeps. |
152
+ | `maxItemHeight` | `number` | `520` | Max height (px) of a panel; taller images scale down keeping ratio.|
153
+ | `sizes` | `string` | `(min-width: 700px) 628px, 82vw` | `sizes` hint for the panel `<img>`. |
154
+ | `lightboxSizes` | `string` | `84vw` | `sizes` hint forwarded to the lightbox images. |
155
+ | `Caption` | `Component` | `TextMorph` | Component used to render the meta title/subtitle. Receives `as` and `children`. Defaults to metamorphosis's morphing `TextMorph`; pass `PlainCaption` (or your own) to opt out. |
156
+ | `lightboxControls` | `boolean` | `false` | Show prev / close / next buttons in the lightbox (on all breakpoints). Off by default — the caption carries the context, and swipe / arrow keys navigate. |
157
+ | `morphCursor` | `boolean` | `false` | Replace the static SVG cursors with a morphing icon cursor: `+` over a panel, `×` over the active lightbox item / overlay, `←` / `→` over the previous / next items. Precise-pointer only; touch and no-JS keep the static cursors. |
158
+
159
+ ## `<Lightbox>` props (internal)
160
+
161
+ The `<Lightbox>` is normally rendered and driven by `<Slider>` during the
162
+ shared-element zoom — you don't usually mount it yourself. If you do, these are
163
+ its props:
164
+
165
+ | Prop | Type | Default | Description |
166
+ | --------------------- | ---------- | ------- | ---------------------------------------------------- |
167
+ | `items` | `Item[]` | — | Same item array passed to `<Slider>`. |
168
+ | `activeIndex` | `number` | — | Index to open on. |
169
+ | `sizes` | `string` | `84vw` | `sizes` hint for the images. |
170
+ | `controls` | `boolean` | `false` | Render the prev / close / next buttons (on all breakpoints). |
171
+ | `onActiveIndexChange` | `Function` | — | Called with the new index as the user scrolls. |
172
+ | `onClose` | `Function` | — | Called to dismiss the lightbox. |
173
+
174
+ Controls are off by default — the caption carries the context, and swipe / drag /
175
+ arrow keys navigate. Enable them with `lightboxControls` on `<Slider>` (or
176
+ `controls` on `<Lightbox>`): a fixed bar with prev / close / next buttons appears
177
+ below the caption on all breakpoints, with prev / next dimmed and disabled at the
178
+ first and last slide.
179
+
180
+ On mobile the lightbox can be dismissed by dragging the image down: the focused
181
+ image follows your finger while the neighbours and backdrop fade out, and past a
182
+ short threshold (or a quick flick) it closes — animating from where you released
183
+ straight back to its slider panel. Below the threshold it springs back.
184
+ Horizontal swipes still navigate.
185
+
186
+ ## Item shape
187
+
188
+ All image fields are plain strings — bring your own CMS / image transform.
189
+
190
+ ```ts
191
+ {
192
+ id, // unique key (falls back to array index)
193
+ title, // shown in the meta line
194
+ meta, // secondary meta line, e.g. "Brand · 2024"
195
+ src, // required: featured image URL
196
+ srcSet?, // responsive srcset
197
+ webpSrcSet?, // webp <source> srcset
198
+ blurDataURL?, // low-quality placeholder (data URI)
199
+ alt?, // defaults to title
200
+ highResSrc?, // hi-res image for the lightbox (falls back to src)
201
+ highResSrcSet?,
202
+ highResWebpSrcSet?,
203
+ video?, // looping muted video URL; autoplays only while active
204
+ }
205
+ ```
206
+
207
+ If `highResSrc` is omitted the lightbox just shows `src` at full size — no
208
+ placeholder/swap layer is rendered.
209
+
210
+ ## Notes
211
+
212
+ - Don't wrap the app in `<StrictMode>` — its dev-only double-invoke of effects
213
+ fights the one-pass measure / view-transition logic. See `demo/src/main.jsx`.
214
+ - The view transition uses a single shared name (`slider-active`), so render one
215
+ `<Slider>` per page if you rely on the zoom animation.
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ 'use strict';var react=require('react'),reactDom=require('react-dom'),react$1=require('motion/react'),react$2=require('@ocarignan/metamorphosis/react'),jsxRuntime=require('react/jsx-runtime');var Me=110,Ie=.55;function he({items:s,activeIndex:T,sizes:E="84vw",Caption:I=react$2.TextMorph,controls:q=false,onActiveIndexChange:N,onClose:R}){let x=react.useRef(null),F=react.useRef({isDragging:false,startX:0,scrollLeft:0}),[a,$]=react.useState(T),[,D]=react.useState(false),d=react.useRef(0),V=react.useRef(null),W=react.useRef({frozen:false,scrollLeft:0}),B=react.useRef(null),j=react.useRef(T),[L,se]=react.useState(false),[ue,y]=react.useState(false),ce=react.useRef(false),Y=react.useRef(false),Z=react.useRef(T);react.useEffect(()=>{let n=setTimeout(()=>{se(true),ce.current=true;let l=x.current;if(l){let _=l.querySelectorAll(".lightbox__item"),A=window.innerWidth/2;_.forEach(v=>{let b=v.getBoundingClientRect(),g=b.left+b.width/2,f=Math.abs(g-A),p=b.width/2,e=Math.max(0,f-p),t=Math.min(e/(window.innerWidth*.3),1);v.style.filter=`blur(0px) brightness(${1-t*.4})`,v.style.transform=`scale(${1-t*.01})`;});}},500),i=setTimeout(()=>y(true),1100);return ()=>{clearTimeout(i),clearTimeout(n);}},[]),react.useEffect(()=>(document.body.style.overflow="hidden",()=>{document.body.style.overflow="";}),[]),react.useLayoutEffect(()=>{let n=x.current;if(!n)return;let i=n.querySelectorAll(".lightbox__item")[j.current];i&&(n.scrollLeft=i.offsetLeft-(n.clientWidth-i.offsetWidth)/2);},[]);let ne=react.useCallback(()=>{Y.current||(Y.current=true,requestAnimationFrame(()=>{Y.current=false;let n=x.current;if(!n)return;let i=n.querySelectorAll(".lightbox__item"),l=window.innerWidth/2,_=new Array(i.length),A=0,v=1/0;for(let b=0;b<i.length;b++){let g=i[b].getBoundingClientRect();_[b]=g;let f=g.left+g.width/2,p=Math.abs(f-l);p<v&&(v=p,A=b);}if(ce.current){let b=window.innerWidth,g=Z.current,f=Math.min(A,g)-2,p=Math.max(A,g)+2;for(let e=Math.max(0,f);e<=Math.min(i.length-1,p);e++){let t=_[e],r=t.left+t.width/2,o=Math.abs(r-l),h=t.width/2,c=Math.max(0,o-h),u=Math.min(c/(b*.3),1);i[e].style.filter=`blur(0px) brightness(${1-u*.4})`,i[e].style.transform=`scale(${1-u*.01})`;}}Z.current=A,N(A),$(A);}));},[N]);react.useEffect(()=>{let n=x.current;if(n)return n.addEventListener("scroll",ne,{passive:true}),()=>n.removeEventListener("scroll",ne)},[ne]);let z=react.useCallback(n=>{let i=x.current;if(!i)return;let l=i.querySelectorAll(".lightbox__item");l[n]&&l[n].scrollIntoView({behavior:"smooth",block:"nearest",inline:"center"});},[]);react.useEffect(()=>{let n=i=>{if(i.key==="Escape")R();else if(i.key==="ArrowRight"){let l=Math.min(a+1,s.length-1);l!==a&&z(l);}else if(i.key==="ArrowLeft"){let l=Math.max(a-1,0);l!==a&&z(l);}};return window.addEventListener("keydown",n),()=>window.removeEventListener("keydown",n)},[a,s.length,R,z]);let re=react.useCallback(()=>{let n=x.current,i=W.current;!n||!i.frozen||(n.style.overflow="",n.scrollLeft=i.scrollLeft,n.style.transform="",n.style.zIndex="",W.current={frozen:false,scrollLeft:0});},[]);react.useEffect(()=>()=>clearTimeout(B.current),[]);let le=react.useCallback(n=>{if(n.pointerType==="touch"){clearTimeout(B.current),d.current=0,V.current={startX:n.clientX,startY:n.clientY,locked:null,dy:0,startTime:Date.now()};return}let i=x.current;i&&(d.current=0,F.current={isDragging:true,startX:n.clientX,scrollLeft:i.scrollLeft,prevX:n.clientX,prevTime:Date.now(),velocity:0},D(true),i.style.scrollSnapType="none",i.setPointerCapture(n.pointerId));},[]),K=react.useCallback(n=>{if(n.pointerType==="touch"){let f=V.current;if(!f)return;let p=n.clientX-f.startX,e=n.clientY-f.startY;if(f.locked===null){if(Math.abs(p)<8&&Math.abs(e)<8)return;f.locked=Math.abs(e)>Math.abs(p)&&e>0?"v":"h";}if(f.locked!=="v")return;f.dy=Math.max(0,e),d.current=f.dy;let t=x.current;if(!t)return;W.current.frozen||(W.current={frozen:true,scrollLeft:t.scrollLeft},t.style.overflow="visible",t.style.transform=`translateX(${-W.current.scrollLeft}px)`,t.style.zIndex="5");let r=Math.max(.85,1-f.dy/1400),o=Math.max(0,1-f.dy/300),h=document.querySelector(".lightbox__backdrop"),c=document.querySelector(".lightbox__meta");t.querySelectorAll(".lightbox__item").forEach(u=>{u.classList.contains("lightbox__item--active")?(u.style.transition="none",u.style.transform=`translateY(${f.dy}px) scale(${r})`):(u.style.transition="none",u.style.opacity=String(o));}),h&&(h.style.transition="none",h.style.opacity=String(o)),c&&(c.style.transition="none",c.style.opacity=String(o));return}let i=F.current;if(!i.isDragging)return;let l=Date.now(),_=l-i.prevTime,A=n.clientX-i.prevX;_>0&&(i.velocity=A/_),i.prevX=n.clientX,i.prevTime=l,d.current=Math.abs(n.clientX-i.startX);let v=x.current,b=i.scrollLeft-(n.clientX-i.startX),g=v.scrollWidth-v.clientWidth;b<0?v.scrollLeft=b*.2:b>g?v.scrollLeft=g+(b-g)*.2:v.scrollLeft=b;},[]),G=react.useCallback(n=>{if(n.pointerType==="touch"){let g=V.current;if(V.current=null,!g||g.locked!=="v")return;let f=x.current,p=f?.querySelector(".lightbox__item--active"),e=document.querySelector(".lightbox__backdrop"),t=document.querySelector(".lightbox__meta"),r=g.dy/Math.max(Date.now()-g.startTime,1);if(g.dy>Me||r>Ie){R({fromDrag:true});return}let o="transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)";p&&(p.style.transition=o,p.style.transform=""),f&&f.querySelectorAll(".lightbox__item").forEach(h=>{h.classList.contains("lightbox__item--active")||(h.style.transition="opacity 0.3s",h.style.opacity="");}),e&&(e.style.transition="opacity 0.3s",e.style.opacity=""),t&&(t.style.transition="opacity 0.3s",t.style.opacity=""),clearTimeout(B.current),B.current=setTimeout(()=>{p&&(p.style.transition=""),re();},300);return}let i=F.current;if(!i.isDragging)return;i.isDragging=false;let l=x.current;if(l.releasePointerCapture(n.pointerId),d.current<5){l.style.scrollSnapType="x mandatory",D(false);let g=document.elementFromPoint(n.clientX,n.clientY)?.closest(".lightbox__item");if(g){let p=Array.from(l.querySelectorAll(".lightbox__item")).indexOf(g);p>=0&&(p===a?R():z(p));}return}let _=-i.velocity*1e3,A=.92,v=performance.now(),b=g=>{let f=(g-v)/1e3;if(v=g,_*=A,l.scrollLeft+=_*f,Math.abs(_)>50)requestAnimationFrame(b);else {let p=l.querySelectorAll(".lightbox__item"),e=window.innerWidth/2,t=0,r=1/0;p.forEach((c,u)=>{let m=c.getBoundingClientRect(),S=m.left+m.width/2,w=Math.abs(S-e);w<r&&(r=w,t=u);});let o=()=>{clearTimeout(h),l.style.scrollSnapType="x mandatory",D(false),l.removeEventListener("scrollend",o);},h=setTimeout(o,300);l.addEventListener("scrollend",o,{once:true}),p[t].scrollIntoView({behavior:"smooth",block:"nearest",inline:"center"});}};requestAnimationFrame(b);},[a,R,z]);react.useEffect(()=>{let n=x.current;if(!n)return;let i=n.querySelectorAll(".lightbox__item"),l=[];return i.forEach((_,A)=>{let v=_.querySelector("video");if(v)if(A===a){let g=setTimeout(()=>{v.currentTime=0,v.play();let f=()=>_.classList.add("lightbox__item--video-ready");v.readyState>=2?f():(v.addEventListener("canplay",f,{once:true}),l.push(()=>v.removeEventListener("canplay",f)));},300);l.push(()=>clearTimeout(g));}else v.pause();}),()=>l.forEach(_=>_())},[a]);let J=react.useCallback(n=>{d.current>=5||(n===a?R():z(n));},[a,R,z]);return jsxRuntime.jsxs("div",{className:`lightbox${L?" lightbox--revealed":""}${q?" lightbox--controls":""}`,children:[jsxRuntime.jsx(react$1.motion.div,{className:"lightbox__backdrop",onClick:R,initial:{opacity:0},animate:{opacity:1},exit:{opacity:0},transition:{duration:.3}}),jsxRuntime.jsx("div",{className:"lightbox__track",ref:x,onPointerDown:le,onPointerMove:K,onPointerUp:G,onPointerCancel:G,onClick:n=>{if(d.current>=5)return;document.elementFromPoint(n.clientX,n.clientY)?.closest(".lightbox__item")||R();},style:{touchAction:"pan-x"},children:s.map((n,i)=>jsxRuntime.jsx(Re,{item:n,index:i,activeIndex:a,sizes:E,staggerDone:ue,onClick:J},n.id??i))}),jsxRuntime.jsxs("div",{className:"lightbox__meta","aria-live":"polite",children:[jsxRuntime.jsx(I,{as:"h2",children:s[a]?.title}),jsxRuntime.jsx(I,{as:"p",children:s[a]?.meta})]}),q&&jsxRuntime.jsxs("div",{className:"lightbox__controls",children:[jsxRuntime.jsx("button",{className:"lightbox__nav",onClick:()=>z(a-1),disabled:a===0,"aria-label":"Previous",children:jsxRuntime.jsx("svg",{width:"14",height:"14",viewBox:"0 0 14 14",fill:"none",xmlns:"http://www.w3.org/2000/svg",children:jsxRuntime.jsx("path",{d:"M9 1L3 7L9 13",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round"})})}),jsxRuntime.jsx("button",{className:"lightbox__close",onClick:R,"aria-label":"Close",children:jsxRuntime.jsx("svg",{width:"14",height:"14",viewBox:"0 0 14 14",fill:"none",xmlns:"http://www.w3.org/2000/svg",children:jsxRuntime.jsx("path",{d:"M1 1L13 13M13 1L1 13",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round"})})}),jsxRuntime.jsx("button",{className:"lightbox__nav",onClick:()=>z(a+1),disabled:a===s.length-1,"aria-label":"Next",children:jsxRuntime.jsx("svg",{width:"14",height:"14",viewBox:"0 0 14 14",fill:"none",xmlns:"http://www.w3.org/2000/svg",children:jsxRuntime.jsx("path",{d:"M5 1L11 7L5 13",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round"})})})]})]})}function Re({item:s,index:T,activeIndex:E,sizes:I,staggerDone:q,onClick:N}){let R=!!s.highResSrc,x=s.video,F=T===E,a=Math.abs(T-E),[$,D]=react.useState(false);return jsxRuntime.jsxs("div",{className:`lightbox__item${F?" lightbox__item--active":""}`,"data-index":T,style:!F&&!q?{transitionDelay:`${a*.06}s`}:void 0,onClick:()=>N(T),children:[jsxRuntime.jsxs("picture",{className:"lightbox__img lightbox__img--low",children:[s.webpSrcSet&&jsxRuntime.jsx("source",{srcSet:s.webpSrcSet,sizes:I,type:"image/webp"}),jsxRuntime.jsx("img",{src:s.src,srcSet:s.srcSet,sizes:I,alt:s.alt||s.title,draggable:false,loading:a<=1?"eager":"lazy",decoding:"async"})]}),R&&jsxRuntime.jsxs("picture",{className:`lightbox__img lightbox__img--high${$?" lightbox__img--ready":""}`,children:[s.highResWebpSrcSet&&jsxRuntime.jsx("source",{srcSet:s.highResWebpSrcSet,sizes:I,type:"image/webp"}),jsxRuntime.jsx("img",{src:s.highResSrc,srcSet:s.highResSrcSet,sizes:I,alt:"","aria-hidden":"true",draggable:false,loading:a<=1?"eager":"lazy",decoding:"async",onLoad:()=>D(true)})]}),x&&jsxRuntime.jsx("video",{src:x,muted:true,playsInline:true,loop:true,preload:"none",draggable:false})]})}function Ae(s,T){if(!s||typeof s.closest!="function")return null;if(!T)return s.closest(".slider__item")?"plus":null;if(s.closest(".lightbox__item--active"))return "close";let E=s.closest(".lightbox__item");if(E){let I=Number(E.dataset.index),q=document.querySelector(".lightbox__item--active"),N=q?Number(q.dataset.index):I;return I<N?"arrow-left":"arrow-right"}return s.closest(".lightbox__backdrop, .lightbox__track")?"close":null}function we({lightboxOpen:s}){let T=react.useRef(null),E=react.useRef(0),I=react.useRef({x:0,y:0}),[q,N]=react.useState(false),[R,x]=react.useState(null),[F,a]=react.useState(false);return react.useEffect(()=>{if(typeof window>"u"||!window.matchMedia)return;let $=window.matchMedia("(pointer: fine)"),D=()=>N($.matches);return D(),$.addEventListener("change",D),()=>$.removeEventListener("change",D)},[]),react.useEffect(()=>{if(!q){a(false);return}let $=d=>{I.current={x:d.clientX,y:d.clientY},E.current||(E.current=requestAnimationFrame(()=>{E.current=0;let W=T.current;if(W){let{x:B,y:j}=I.current;W.style.transform=`translate(${B}px, ${j}px) translate(-50%, -50%)`;}}));let V=Ae(d.target,s);V?(x(V),a(true)):a(false);},D=d=>{d.relatedTarget||a(false);};return window.addEventListener("pointermove",$,{passive:true}),window.addEventListener("pointerout",D,{passive:true}),()=>{window.removeEventListener("pointermove",$),window.removeEventListener("pointerout",D),E.current&&cancelAnimationFrame(E.current),E.current=0;}},[q,s]),q?jsxRuntime.jsx("div",{ref:T,className:`morph-cursor${F?" morph-cursor--visible":""}`,"aria-hidden":"true",children:R&&jsxRuntime.jsx(react$2.IconMorph,{name:R,size:20,strokeWidth:1.75,color:"#fff",duration:250})}):null}var Pe=({as:s="span",children:T})=>jsxRuntime.jsx(s,{children:T}),Xe={initial:{},animate:{transition:{staggerChildren:.06}}},_e={initial:{opacity:0,y:8},animate:{opacity:1,y:0,transition:{duration:1,ease:[0,.55,.45,1]}}};function Ne({items:s,contentWidth:T=628,gap:E=32,columns:I=4,metaOffsetColumns:q=0,sideMargin:N=24,maxItemHeight:R=520,sizes:x="(min-width: 700px) 628px, 82vw",lightboxSizes:F="84vw",Caption:a=react$2.TextMorph,lightboxControls:$=false,morphCursor:D=false}){let d=react.useRef(null),V=react.useRef(null),W=react.useRef(false),B=react.useRef(null),j=react.useRef({isDragging:false,startX:0,scrollLeft:0}),[L,se]=react.useState(0),ue=react.useRef(0);ue.current=L;let[y,ce]=react.useState({inset:0,itemWidth:0,metaInset:0,metaInsetRight:0,endPad:0,isMobile:false}),[Y,Z]=react.useState(false),[ne,z]=react.useState(false),re=react.useRef(0),le=react.useRef(false),K=react.useRef(null),G=react.useRef(false);react.useEffect(()=>{let e=()=>{let o=window.innerWidth,h=window.matchMedia("(min-width: 700px)").matches,c=document.querySelector(".subgrid"),u=c?.closest(".grid")||document.querySelector(".grid");if(h){let m,S,w,M;if(c&&u){let Q=c.getBoundingClientRect();m=Q.left,S=Q.width,w=parseInt(getComputedStyle(u).getPropertyValue("--columns"),10)||I,M=parseFloat(getComputedStyle(c).columnGap)||E;}else S=Math.min(T,o-2*N),m=(o-S)/2,w=I,M=E;let C=(S-(w-1)*M)/w,P=m-12;ce({inset:P,itemWidth:S+24,metaInset:P+q*(C+M),metaInsetRight:P,endPad:o-P,isMobile:false});}else {let m;c&&u?m=c.getBoundingClientRect().left-u.getBoundingClientRect().left:m=N,ce({inset:m,itemWidth:o-2*m,metaInset:m,metaInsetRight:m,endPad:m,isMobile:true});}},t,r=()=>{clearTimeout(t),t=setTimeout(e,100);};return e(),window.addEventListener("resize",r),()=>{window.removeEventListener("resize",r),clearTimeout(t);}},[T,E,I,q,N]),react.useEffect(()=>{let e=d.current;e&&(e.scrollLeft=0);},[y.inset]);let J=react.useCallback(e=>{let t=d.current;if(!t||!y.itemWidth)return;let r=t.querySelectorAll(".slider__item");r[e]&&r[e].scrollIntoView({behavior:"smooth",block:"nearest",inline:y.isMobile?"center":"start"});},[y.itemWidth,y.isMobile]);react.useEffect(()=>{let e=t=>{if(!(t.key!=="ArrowRight"&&t.key!=="ArrowLeft"||Y||!(W.current||V.current&&V.current.contains(document.activeElement))))if(t.key==="ArrowRight"){let o=Math.min(L+1,s.length-1);o!==L&&(t.preventDefault(),J(o));}else {let o=Math.max(L-1,0);o!==L&&(t.preventDefault(),J(o));}};return window.addEventListener("keydown",e),()=>window.removeEventListener("keydown",e)},[L,s.length,Y,J]);let n=react.useCallback(e=>{let t=e.querySelectorAll(".slider__item"),r=window.innerWidth/2,o=0,h=1/0;return t.forEach((c,u)=>{let m=c.getBoundingClientRect(),S=m.left+m.width/2,w=Math.abs(S-r);w<h&&(h=w,o=u);let M=m.width/2,C=Math.max(0,w-M),P=Math.min(C/(window.innerWidth*.3),1),Q=c.querySelector(".slider__item-inner");Q&&(Q.style.transformOrigin=S<r?"right center":"left center",Q.style.transform=`scale(${1-P*.05})`,Q.style.filter=`brightness(${1-P*.15})`);}),o},[]),i=react.useCallback(()=>{if(d.current&&!le.current){if(y.isMobile){if(G.current)return;G.current=true,requestAnimationFrame(()=>{G.current=false;let t=d.current;if(!t)return;let r=n(t);se(r),clearTimeout(B.current);});return}G.current||(G.current=true,requestAnimationFrame(()=>{G.current=false;let t=d.current;if(!t)return;let r=t.querySelectorAll(".slider__item"),o=y.inset,h=0,c=1/0;r.forEach((u,m)=>{let S=Math.abs(u.getBoundingClientRect().left-o);S<c&&(c=S,h=m);}),se(h);}));}},[y.inset,y.isMobile,n]);react.useEffect(()=>{let e=d.current;if(e)return e.addEventListener("scroll",i,{passive:true}),()=>e.removeEventListener("scroll",i)},[i]),react.useEffect(()=>{let e=d.current;e&&(y.isMobile?requestAnimationFrame(()=>n(e)):e.querySelectorAll(".slider__item-inner").forEach(t=>{t.style.transform="",t.style.filter="",t.style.transformOrigin="";}));},[y.isMobile,n]);let l=react.useCallback(()=>{let e=ue.current,t=d.current?.querySelectorAll(".slider__item");if(t?.[e]&&!K.current)if(document.startViewTransition){t[e].classList.add("slider__item--transitioning"),t[e].style.viewTransitionName="slider-active",document.documentElement.style.viewTransitionName="none";let r=document.startViewTransition(()=>{t[e].style.viewTransitionName="",reactDom.flushSync(()=>Z(true));});K.current=r,r.finished.then(()=>{K.current=null,t[e].classList.remove("slider__item--transitioning"),document.documentElement.style.viewTransitionName="";});}else Z(true);},[]),_=react.useCallback(e=>{if(e===L){l();return}let t=d.current;J(e);let r=()=>{clearTimeout(o),t?.removeEventListener("scrollend",r),l();},o=setTimeout(r,600);t?.addEventListener("scrollend",r,{once:true});},[L,l,J]),A=react.useCallback(e=>{if(e.pointerType==="touch")return;let t=d.current;t&&(re.current=0,j.current={isDragging:true,startX:e.clientX,scrollLeft:t.scrollLeft,prevX:e.clientX,prevTime:Date.now(),velocity:0},t.style.scrollSnapType="none",t.setPointerCapture(e.pointerId));},[]),v=react.useCallback(e=>{let t=j.current;if(!t.isDragging)return;let r=Date.now(),o=r-t.prevTime,h=e.clientX-t.prevX;o>0&&(t.velocity=h/o),t.prevX=e.clientX,t.prevTime=r,re.current=Math.abs(e.clientX-t.startX),d.current.scrollLeft=t.scrollLeft-(e.clientX-t.startX);},[]),b=react.useCallback(e=>{let t=j.current;if(!t.isDragging)return;t.isDragging=false;let r=d.current;if(r.releasePointerCapture(e.pointerId),re.current<5){r.style.scrollSnapType="x mandatory";let m=document.elementFromPoint(e.clientX,e.clientY)?.closest(".slider__item");if(m){let w=Array.from(r.querySelectorAll(".slider__item")).indexOf(m);w>=0&&_(w);}return}let o=-t.velocity*1e3,h=.92,c=performance.now(),u=m=>{let S=(m-c)/1e3;if(c=m,o*=h,r.scrollLeft+=o*S,Math.abs(o)>50)requestAnimationFrame(u);else {let w=r.querySelectorAll(".slider__item"),M=0,C=1/0;w.forEach((Le,Se)=>{let ve=Math.abs(Le.getBoundingClientRect().left-y.inset);ve<C&&(C=ve,M=Se);});let P=()=>{clearTimeout(Q),r.style.scrollSnapType="x mandatory",r.removeEventListener("scrollend",P);},Q=setTimeout(P,300);r.addEventListener("scrollend",P,{once:true}),w[M].scrollIntoView({behavior:"smooth",block:"nearest",inline:"start"});}};requestAnimationFrame(u);},[y.inset,_]);react.useEffect(()=>{if(Y){let e=setTimeout(()=>z(true),300);return ()=>clearTimeout(e)}else z(false);},[Y]),react.useEffect(()=>{let e=d.current;if(!e)return;let t=e.querySelectorAll(".slider__item"),r=[];return t.forEach((o,h)=>{let c=o.querySelector("video");if(c)if(ne)c.pause(),c.style.visibility="hidden",o.classList.remove("slider__item--video-ready");else if(c.style.visibility="",h===L){c.currentTime=0,c.play();let u=()=>o.classList.add("slider__item--video-ready");c.readyState>=2?u():(c.addEventListener("canplay",u,{once:true}),r.push(()=>c.removeEventListener("canplay",u)));}else c.pause(),o.classList.remove("slider__item--video-ready");}),()=>r.forEach(o=>o())},[L,ne]);let g=react.useCallback(e=>{let t=e?.fromDrag===true;if(K.current)return;let r=d.current?.querySelectorAll(".slider__item");if(!r?.[L]){Z(false);return}let o=document.querySelector(".lightbox");o&&!t&&o.classList.add("lightbox--closing");let h=y.isMobile?"center":"start",c=d.current,u=()=>{let w=r[L];if(!w)return;let M=w.getBoundingClientRect(),C=h==="center"?window.innerWidth/2:y.inset,P=h==="center"?M.left+M.width/2:M.left;Math.abs(P-C)<4||w.scrollIntoView({behavior:"instant",block:"nearest",inline:h});};c?.addEventListener("scroll",u);let m=()=>c?.removeEventListener("scroll",u),S=()=>{if(K.current){m();return}if(document.startViewTransition){r.forEach(M=>{let C=M.querySelector("video");C&&(C.style.visibility="");}),document.documentElement.style.viewTransitionName="none";let w=document.startViewTransition(()=>{let M=document.querySelector(".lightbox__item--active");M&&(M.style.viewTransitionName="none");let C=document.querySelector(".lightbox");C&&(C.style.display="none"),u(),r[L].style.viewTransitionName="slider-active",reactDom.flushSync(()=>Z(false));});K.current=w,w.finished.then(()=>{K.current=null,r[L].style.viewTransitionName="",document.documentElement.style.viewTransitionName="",m();});}else u(),Z(false),setTimeout(m,400);};t?requestAnimationFrame(S):o?setTimeout(S,350):S();},[L,y.isMobile]),f=react.useCallback(e=>{se(e),le.current=true,J(e),clearTimeout(B.current),B.current=setTimeout(()=>{le.current=false;},300);},[J]),p=s[L];return jsxRuntime.jsxs(react$1.motion.div,{className:`slider${D?" slider--morph-cursor":""}`,ref:V,variants:Xe,onPointerEnter:()=>W.current=true,onPointerLeave:()=>W.current=false,children:[jsxRuntime.jsx(react$1.motion.div,{className:"slider__track",ref:d,tabIndex:-1,onPointerDown:A,onPointerMove:v,onPointerUp:b,onPointerCancel:b,style:{paddingLeft:`${y.inset}px`,paddingRight:`${y.endPad}px`,scrollPaddingLeft:y.isMobile?void 0:`${y.inset}px`,touchAction:"pan-x pan-y"},children:s.map((e,t)=>{let r=e.id??t;return jsxRuntime.jsx(react$1.motion.div,{className:`slider__item${t===L?" slider__item--active":""}`,variants:_e,role:"button",tabIndex:0,"aria-label":e.title,style:{"--item-max-w":`${y.itemWidth}px`,"--item-max-h":`${R}px`,cursor:D?"none":"zoom-in"},onClick:()=>{re.current<5&&_(t);},onKeyDown:o=>{(o.key==="Enter"||o.key===" ")&&(o.preventDefault(),_(t));},children:jsxRuntime.jsxs("div",{className:"slider__item-inner",children:[jsxRuntime.jsxs("picture",{children:[e.webpSrcSet&&jsxRuntime.jsx("source",{srcSet:e.webpSrcSet,sizes:x,type:"image/webp"}),jsxRuntime.jsx("img",{src:e.src,srcSet:e.srcSet,sizes:x,alt:e.alt||e.title,draggable:false,fetchPriority:t===0?"high":void 0,loading:t<=1?"eager":"lazy",decoding:t<=1?"sync":"async",style:e.blurDataURL?{backgroundImage:`url(${e.blurDataURL})`,backgroundSize:"cover",backgroundRepeat:"no-repeat",backgroundPosition:"center"}:void 0})]}),e.video&&jsxRuntime.jsx("video",{src:e.video,muted:true,playsInline:true,loop:true,preload:"none",draggable:false})]})},r)})}),jsxRuntime.jsx(react$1.motion.div,{className:"slider__meta",style:{paddingLeft:`${y.metaInset}px`,paddingRight:`${y.metaInsetRight}px`},variants:_e,children:jsxRuntime.jsxs("div",{className:"slider__meta-inner",children:[jsxRuntime.jsx(a,{as:"h3",children:p?.title}),jsxRuntime.jsx("br",{}),jsxRuntime.jsx(a,{as:"p",children:p?.meta})]})}),jsxRuntime.jsx(react$1.AnimatePresence,{children:Y&&jsxRuntime.jsx(he,{items:s,activeIndex:L,sizes:F,Caption:a,controls:$,onActiveIndexChange:f,onClose:g})}),D&&jsxRuntime.jsx(we,{lightboxOpen:Y})]})}
2
+ exports.Lightbox=he;exports.PlainCaption=Pe;exports.Slider=Ne;
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import {useRef,useState,useEffect,useLayoutEffect,useCallback}from'react';import {flushSync}from'react-dom';import {motion,AnimatePresence}from'motion/react';import {TextMorph,IconMorph}from'@ocarignan/metamorphosis/react';import {jsxs,jsx}from'react/jsx-runtime';var Me=110,Ie=.55;function he({items:s,activeIndex:T,sizes:E="84vw",Caption:I=TextMorph,controls:q=false,onActiveIndexChange:N,onClose:R}){let x=useRef(null),F=useRef({isDragging:false,startX:0,scrollLeft:0}),[a,$]=useState(T),[,D]=useState(false),d=useRef(0),V=useRef(null),W=useRef({frozen:false,scrollLeft:0}),B=useRef(null),j=useRef(T),[L,se]=useState(false),[ue,y]=useState(false),ce=useRef(false),Y=useRef(false),Z=useRef(T);useEffect(()=>{let n=setTimeout(()=>{se(true),ce.current=true;let l=x.current;if(l){let _=l.querySelectorAll(".lightbox__item"),A=window.innerWidth/2;_.forEach(v=>{let b=v.getBoundingClientRect(),g=b.left+b.width/2,f=Math.abs(g-A),p=b.width/2,e=Math.max(0,f-p),t=Math.min(e/(window.innerWidth*.3),1);v.style.filter=`blur(0px) brightness(${1-t*.4})`,v.style.transform=`scale(${1-t*.01})`;});}},500),i=setTimeout(()=>y(true),1100);return ()=>{clearTimeout(i),clearTimeout(n);}},[]),useEffect(()=>(document.body.style.overflow="hidden",()=>{document.body.style.overflow="";}),[]),useLayoutEffect(()=>{let n=x.current;if(!n)return;let i=n.querySelectorAll(".lightbox__item")[j.current];i&&(n.scrollLeft=i.offsetLeft-(n.clientWidth-i.offsetWidth)/2);},[]);let ne=useCallback(()=>{Y.current||(Y.current=true,requestAnimationFrame(()=>{Y.current=false;let n=x.current;if(!n)return;let i=n.querySelectorAll(".lightbox__item"),l=window.innerWidth/2,_=new Array(i.length),A=0,v=1/0;for(let b=0;b<i.length;b++){let g=i[b].getBoundingClientRect();_[b]=g;let f=g.left+g.width/2,p=Math.abs(f-l);p<v&&(v=p,A=b);}if(ce.current){let b=window.innerWidth,g=Z.current,f=Math.min(A,g)-2,p=Math.max(A,g)+2;for(let e=Math.max(0,f);e<=Math.min(i.length-1,p);e++){let t=_[e],r=t.left+t.width/2,o=Math.abs(r-l),h=t.width/2,c=Math.max(0,o-h),u=Math.min(c/(b*.3),1);i[e].style.filter=`blur(0px) brightness(${1-u*.4})`,i[e].style.transform=`scale(${1-u*.01})`;}}Z.current=A,N(A),$(A);}));},[N]);useEffect(()=>{let n=x.current;if(n)return n.addEventListener("scroll",ne,{passive:true}),()=>n.removeEventListener("scroll",ne)},[ne]);let z=useCallback(n=>{let i=x.current;if(!i)return;let l=i.querySelectorAll(".lightbox__item");l[n]&&l[n].scrollIntoView({behavior:"smooth",block:"nearest",inline:"center"});},[]);useEffect(()=>{let n=i=>{if(i.key==="Escape")R();else if(i.key==="ArrowRight"){let l=Math.min(a+1,s.length-1);l!==a&&z(l);}else if(i.key==="ArrowLeft"){let l=Math.max(a-1,0);l!==a&&z(l);}};return window.addEventListener("keydown",n),()=>window.removeEventListener("keydown",n)},[a,s.length,R,z]);let re=useCallback(()=>{let n=x.current,i=W.current;!n||!i.frozen||(n.style.overflow="",n.scrollLeft=i.scrollLeft,n.style.transform="",n.style.zIndex="",W.current={frozen:false,scrollLeft:0});},[]);useEffect(()=>()=>clearTimeout(B.current),[]);let le=useCallback(n=>{if(n.pointerType==="touch"){clearTimeout(B.current),d.current=0,V.current={startX:n.clientX,startY:n.clientY,locked:null,dy:0,startTime:Date.now()};return}let i=x.current;i&&(d.current=0,F.current={isDragging:true,startX:n.clientX,scrollLeft:i.scrollLeft,prevX:n.clientX,prevTime:Date.now(),velocity:0},D(true),i.style.scrollSnapType="none",i.setPointerCapture(n.pointerId));},[]),K=useCallback(n=>{if(n.pointerType==="touch"){let f=V.current;if(!f)return;let p=n.clientX-f.startX,e=n.clientY-f.startY;if(f.locked===null){if(Math.abs(p)<8&&Math.abs(e)<8)return;f.locked=Math.abs(e)>Math.abs(p)&&e>0?"v":"h";}if(f.locked!=="v")return;f.dy=Math.max(0,e),d.current=f.dy;let t=x.current;if(!t)return;W.current.frozen||(W.current={frozen:true,scrollLeft:t.scrollLeft},t.style.overflow="visible",t.style.transform=`translateX(${-W.current.scrollLeft}px)`,t.style.zIndex="5");let r=Math.max(.85,1-f.dy/1400),o=Math.max(0,1-f.dy/300),h=document.querySelector(".lightbox__backdrop"),c=document.querySelector(".lightbox__meta");t.querySelectorAll(".lightbox__item").forEach(u=>{u.classList.contains("lightbox__item--active")?(u.style.transition="none",u.style.transform=`translateY(${f.dy}px) scale(${r})`):(u.style.transition="none",u.style.opacity=String(o));}),h&&(h.style.transition="none",h.style.opacity=String(o)),c&&(c.style.transition="none",c.style.opacity=String(o));return}let i=F.current;if(!i.isDragging)return;let l=Date.now(),_=l-i.prevTime,A=n.clientX-i.prevX;_>0&&(i.velocity=A/_),i.prevX=n.clientX,i.prevTime=l,d.current=Math.abs(n.clientX-i.startX);let v=x.current,b=i.scrollLeft-(n.clientX-i.startX),g=v.scrollWidth-v.clientWidth;b<0?v.scrollLeft=b*.2:b>g?v.scrollLeft=g+(b-g)*.2:v.scrollLeft=b;},[]),G=useCallback(n=>{if(n.pointerType==="touch"){let g=V.current;if(V.current=null,!g||g.locked!=="v")return;let f=x.current,p=f?.querySelector(".lightbox__item--active"),e=document.querySelector(".lightbox__backdrop"),t=document.querySelector(".lightbox__meta"),r=g.dy/Math.max(Date.now()-g.startTime,1);if(g.dy>Me||r>Ie){R({fromDrag:true});return}let o="transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)";p&&(p.style.transition=o,p.style.transform=""),f&&f.querySelectorAll(".lightbox__item").forEach(h=>{h.classList.contains("lightbox__item--active")||(h.style.transition="opacity 0.3s",h.style.opacity="");}),e&&(e.style.transition="opacity 0.3s",e.style.opacity=""),t&&(t.style.transition="opacity 0.3s",t.style.opacity=""),clearTimeout(B.current),B.current=setTimeout(()=>{p&&(p.style.transition=""),re();},300);return}let i=F.current;if(!i.isDragging)return;i.isDragging=false;let l=x.current;if(l.releasePointerCapture(n.pointerId),d.current<5){l.style.scrollSnapType="x mandatory",D(false);let g=document.elementFromPoint(n.clientX,n.clientY)?.closest(".lightbox__item");if(g){let p=Array.from(l.querySelectorAll(".lightbox__item")).indexOf(g);p>=0&&(p===a?R():z(p));}return}let _=-i.velocity*1e3,A=.92,v=performance.now(),b=g=>{let f=(g-v)/1e3;if(v=g,_*=A,l.scrollLeft+=_*f,Math.abs(_)>50)requestAnimationFrame(b);else {let p=l.querySelectorAll(".lightbox__item"),e=window.innerWidth/2,t=0,r=1/0;p.forEach((c,u)=>{let m=c.getBoundingClientRect(),S=m.left+m.width/2,w=Math.abs(S-e);w<r&&(r=w,t=u);});let o=()=>{clearTimeout(h),l.style.scrollSnapType="x mandatory",D(false),l.removeEventListener("scrollend",o);},h=setTimeout(o,300);l.addEventListener("scrollend",o,{once:true}),p[t].scrollIntoView({behavior:"smooth",block:"nearest",inline:"center"});}};requestAnimationFrame(b);},[a,R,z]);useEffect(()=>{let n=x.current;if(!n)return;let i=n.querySelectorAll(".lightbox__item"),l=[];return i.forEach((_,A)=>{let v=_.querySelector("video");if(v)if(A===a){let g=setTimeout(()=>{v.currentTime=0,v.play();let f=()=>_.classList.add("lightbox__item--video-ready");v.readyState>=2?f():(v.addEventListener("canplay",f,{once:true}),l.push(()=>v.removeEventListener("canplay",f)));},300);l.push(()=>clearTimeout(g));}else v.pause();}),()=>l.forEach(_=>_())},[a]);let J=useCallback(n=>{d.current>=5||(n===a?R():z(n));},[a,R,z]);return jsxs("div",{className:`lightbox${L?" lightbox--revealed":""}${q?" lightbox--controls":""}`,children:[jsx(motion.div,{className:"lightbox__backdrop",onClick:R,initial:{opacity:0},animate:{opacity:1},exit:{opacity:0},transition:{duration:.3}}),jsx("div",{className:"lightbox__track",ref:x,onPointerDown:le,onPointerMove:K,onPointerUp:G,onPointerCancel:G,onClick:n=>{if(d.current>=5)return;document.elementFromPoint(n.clientX,n.clientY)?.closest(".lightbox__item")||R();},style:{touchAction:"pan-x"},children:s.map((n,i)=>jsx(Re,{item:n,index:i,activeIndex:a,sizes:E,staggerDone:ue,onClick:J},n.id??i))}),jsxs("div",{className:"lightbox__meta","aria-live":"polite",children:[jsx(I,{as:"h2",children:s[a]?.title}),jsx(I,{as:"p",children:s[a]?.meta})]}),q&&jsxs("div",{className:"lightbox__controls",children:[jsx("button",{className:"lightbox__nav",onClick:()=>z(a-1),disabled:a===0,"aria-label":"Previous",children:jsx("svg",{width:"14",height:"14",viewBox:"0 0 14 14",fill:"none",xmlns:"http://www.w3.org/2000/svg",children:jsx("path",{d:"M9 1L3 7L9 13",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round"})})}),jsx("button",{className:"lightbox__close",onClick:R,"aria-label":"Close",children:jsx("svg",{width:"14",height:"14",viewBox:"0 0 14 14",fill:"none",xmlns:"http://www.w3.org/2000/svg",children:jsx("path",{d:"M1 1L13 13M13 1L1 13",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round"})})}),jsx("button",{className:"lightbox__nav",onClick:()=>z(a+1),disabled:a===s.length-1,"aria-label":"Next",children:jsx("svg",{width:"14",height:"14",viewBox:"0 0 14 14",fill:"none",xmlns:"http://www.w3.org/2000/svg",children:jsx("path",{d:"M5 1L11 7L5 13",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round"})})})]})]})}function Re({item:s,index:T,activeIndex:E,sizes:I,staggerDone:q,onClick:N}){let R=!!s.highResSrc,x=s.video,F=T===E,a=Math.abs(T-E),[$,D]=useState(false);return jsxs("div",{className:`lightbox__item${F?" lightbox__item--active":""}`,"data-index":T,style:!F&&!q?{transitionDelay:`${a*.06}s`}:void 0,onClick:()=>N(T),children:[jsxs("picture",{className:"lightbox__img lightbox__img--low",children:[s.webpSrcSet&&jsx("source",{srcSet:s.webpSrcSet,sizes:I,type:"image/webp"}),jsx("img",{src:s.src,srcSet:s.srcSet,sizes:I,alt:s.alt||s.title,draggable:false,loading:a<=1?"eager":"lazy",decoding:"async"})]}),R&&jsxs("picture",{className:`lightbox__img lightbox__img--high${$?" lightbox__img--ready":""}`,children:[s.highResWebpSrcSet&&jsx("source",{srcSet:s.highResWebpSrcSet,sizes:I,type:"image/webp"}),jsx("img",{src:s.highResSrc,srcSet:s.highResSrcSet,sizes:I,alt:"","aria-hidden":"true",draggable:false,loading:a<=1?"eager":"lazy",decoding:"async",onLoad:()=>D(true)})]}),x&&jsx("video",{src:x,muted:true,playsInline:true,loop:true,preload:"none",draggable:false})]})}function Ae(s,T){if(!s||typeof s.closest!="function")return null;if(!T)return s.closest(".slider__item")?"plus":null;if(s.closest(".lightbox__item--active"))return "close";let E=s.closest(".lightbox__item");if(E){let I=Number(E.dataset.index),q=document.querySelector(".lightbox__item--active"),N=q?Number(q.dataset.index):I;return I<N?"arrow-left":"arrow-right"}return s.closest(".lightbox__backdrop, .lightbox__track")?"close":null}function we({lightboxOpen:s}){let T=useRef(null),E=useRef(0),I=useRef({x:0,y:0}),[q,N]=useState(false),[R,x]=useState(null),[F,a]=useState(false);return useEffect(()=>{if(typeof window>"u"||!window.matchMedia)return;let $=window.matchMedia("(pointer: fine)"),D=()=>N($.matches);return D(),$.addEventListener("change",D),()=>$.removeEventListener("change",D)},[]),useEffect(()=>{if(!q){a(false);return}let $=d=>{I.current={x:d.clientX,y:d.clientY},E.current||(E.current=requestAnimationFrame(()=>{E.current=0;let W=T.current;if(W){let{x:B,y:j}=I.current;W.style.transform=`translate(${B}px, ${j}px) translate(-50%, -50%)`;}}));let V=Ae(d.target,s);V?(x(V),a(true)):a(false);},D=d=>{d.relatedTarget||a(false);};return window.addEventListener("pointermove",$,{passive:true}),window.addEventListener("pointerout",D,{passive:true}),()=>{window.removeEventListener("pointermove",$),window.removeEventListener("pointerout",D),E.current&&cancelAnimationFrame(E.current),E.current=0;}},[q,s]),q?jsx("div",{ref:T,className:`morph-cursor${F?" morph-cursor--visible":""}`,"aria-hidden":"true",children:R&&jsx(IconMorph,{name:R,size:20,strokeWidth:1.75,color:"#fff",duration:250})}):null}var Pe=({as:s="span",children:T})=>jsx(s,{children:T}),Xe={initial:{},animate:{transition:{staggerChildren:.06}}},_e={initial:{opacity:0,y:8},animate:{opacity:1,y:0,transition:{duration:1,ease:[0,.55,.45,1]}}};function Ne({items:s,contentWidth:T=628,gap:E=32,columns:I=4,metaOffsetColumns:q=0,sideMargin:N=24,maxItemHeight:R=520,sizes:x="(min-width: 700px) 628px, 82vw",lightboxSizes:F="84vw",Caption:a=TextMorph,lightboxControls:$=false,morphCursor:D=false}){let d=useRef(null),V=useRef(null),W=useRef(false),B=useRef(null),j=useRef({isDragging:false,startX:0,scrollLeft:0}),[L,se]=useState(0),ue=useRef(0);ue.current=L;let[y,ce]=useState({inset:0,itemWidth:0,metaInset:0,metaInsetRight:0,endPad:0,isMobile:false}),[Y,Z]=useState(false),[ne,z]=useState(false),re=useRef(0),le=useRef(false),K=useRef(null),G=useRef(false);useEffect(()=>{let e=()=>{let o=window.innerWidth,h=window.matchMedia("(min-width: 700px)").matches,c=document.querySelector(".subgrid"),u=c?.closest(".grid")||document.querySelector(".grid");if(h){let m,S,w,M;if(c&&u){let Q=c.getBoundingClientRect();m=Q.left,S=Q.width,w=parseInt(getComputedStyle(u).getPropertyValue("--columns"),10)||I,M=parseFloat(getComputedStyle(c).columnGap)||E;}else S=Math.min(T,o-2*N),m=(o-S)/2,w=I,M=E;let C=(S-(w-1)*M)/w,P=m-12;ce({inset:P,itemWidth:S+24,metaInset:P+q*(C+M),metaInsetRight:P,endPad:o-P,isMobile:false});}else {let m;c&&u?m=c.getBoundingClientRect().left-u.getBoundingClientRect().left:m=N,ce({inset:m,itemWidth:o-2*m,metaInset:m,metaInsetRight:m,endPad:m,isMobile:true});}},t,r=()=>{clearTimeout(t),t=setTimeout(e,100);};return e(),window.addEventListener("resize",r),()=>{window.removeEventListener("resize",r),clearTimeout(t);}},[T,E,I,q,N]),useEffect(()=>{let e=d.current;e&&(e.scrollLeft=0);},[y.inset]);let J=useCallback(e=>{let t=d.current;if(!t||!y.itemWidth)return;let r=t.querySelectorAll(".slider__item");r[e]&&r[e].scrollIntoView({behavior:"smooth",block:"nearest",inline:y.isMobile?"center":"start"});},[y.itemWidth,y.isMobile]);useEffect(()=>{let e=t=>{if(!(t.key!=="ArrowRight"&&t.key!=="ArrowLeft"||Y||!(W.current||V.current&&V.current.contains(document.activeElement))))if(t.key==="ArrowRight"){let o=Math.min(L+1,s.length-1);o!==L&&(t.preventDefault(),J(o));}else {let o=Math.max(L-1,0);o!==L&&(t.preventDefault(),J(o));}};return window.addEventListener("keydown",e),()=>window.removeEventListener("keydown",e)},[L,s.length,Y,J]);let n=useCallback(e=>{let t=e.querySelectorAll(".slider__item"),r=window.innerWidth/2,o=0,h=1/0;return t.forEach((c,u)=>{let m=c.getBoundingClientRect(),S=m.left+m.width/2,w=Math.abs(S-r);w<h&&(h=w,o=u);let M=m.width/2,C=Math.max(0,w-M),P=Math.min(C/(window.innerWidth*.3),1),Q=c.querySelector(".slider__item-inner");Q&&(Q.style.transformOrigin=S<r?"right center":"left center",Q.style.transform=`scale(${1-P*.05})`,Q.style.filter=`brightness(${1-P*.15})`);}),o},[]),i=useCallback(()=>{if(d.current&&!le.current){if(y.isMobile){if(G.current)return;G.current=true,requestAnimationFrame(()=>{G.current=false;let t=d.current;if(!t)return;let r=n(t);se(r),clearTimeout(B.current);});return}G.current||(G.current=true,requestAnimationFrame(()=>{G.current=false;let t=d.current;if(!t)return;let r=t.querySelectorAll(".slider__item"),o=y.inset,h=0,c=1/0;r.forEach((u,m)=>{let S=Math.abs(u.getBoundingClientRect().left-o);S<c&&(c=S,h=m);}),se(h);}));}},[y.inset,y.isMobile,n]);useEffect(()=>{let e=d.current;if(e)return e.addEventListener("scroll",i,{passive:true}),()=>e.removeEventListener("scroll",i)},[i]),useEffect(()=>{let e=d.current;e&&(y.isMobile?requestAnimationFrame(()=>n(e)):e.querySelectorAll(".slider__item-inner").forEach(t=>{t.style.transform="",t.style.filter="",t.style.transformOrigin="";}));},[y.isMobile,n]);let l=useCallback(()=>{let e=ue.current,t=d.current?.querySelectorAll(".slider__item");if(t?.[e]&&!K.current)if(document.startViewTransition){t[e].classList.add("slider__item--transitioning"),t[e].style.viewTransitionName="slider-active",document.documentElement.style.viewTransitionName="none";let r=document.startViewTransition(()=>{t[e].style.viewTransitionName="",flushSync(()=>Z(true));});K.current=r,r.finished.then(()=>{K.current=null,t[e].classList.remove("slider__item--transitioning"),document.documentElement.style.viewTransitionName="";});}else Z(true);},[]),_=useCallback(e=>{if(e===L){l();return}let t=d.current;J(e);let r=()=>{clearTimeout(o),t?.removeEventListener("scrollend",r),l();},o=setTimeout(r,600);t?.addEventListener("scrollend",r,{once:true});},[L,l,J]),A=useCallback(e=>{if(e.pointerType==="touch")return;let t=d.current;t&&(re.current=0,j.current={isDragging:true,startX:e.clientX,scrollLeft:t.scrollLeft,prevX:e.clientX,prevTime:Date.now(),velocity:0},t.style.scrollSnapType="none",t.setPointerCapture(e.pointerId));},[]),v=useCallback(e=>{let t=j.current;if(!t.isDragging)return;let r=Date.now(),o=r-t.prevTime,h=e.clientX-t.prevX;o>0&&(t.velocity=h/o),t.prevX=e.clientX,t.prevTime=r,re.current=Math.abs(e.clientX-t.startX),d.current.scrollLeft=t.scrollLeft-(e.clientX-t.startX);},[]),b=useCallback(e=>{let t=j.current;if(!t.isDragging)return;t.isDragging=false;let r=d.current;if(r.releasePointerCapture(e.pointerId),re.current<5){r.style.scrollSnapType="x mandatory";let m=document.elementFromPoint(e.clientX,e.clientY)?.closest(".slider__item");if(m){let w=Array.from(r.querySelectorAll(".slider__item")).indexOf(m);w>=0&&_(w);}return}let o=-t.velocity*1e3,h=.92,c=performance.now(),u=m=>{let S=(m-c)/1e3;if(c=m,o*=h,r.scrollLeft+=o*S,Math.abs(o)>50)requestAnimationFrame(u);else {let w=r.querySelectorAll(".slider__item"),M=0,C=1/0;w.forEach((Le,Se)=>{let ve=Math.abs(Le.getBoundingClientRect().left-y.inset);ve<C&&(C=ve,M=Se);});let P=()=>{clearTimeout(Q),r.style.scrollSnapType="x mandatory",r.removeEventListener("scrollend",P);},Q=setTimeout(P,300);r.addEventListener("scrollend",P,{once:true}),w[M].scrollIntoView({behavior:"smooth",block:"nearest",inline:"start"});}};requestAnimationFrame(u);},[y.inset,_]);useEffect(()=>{if(Y){let e=setTimeout(()=>z(true),300);return ()=>clearTimeout(e)}else z(false);},[Y]),useEffect(()=>{let e=d.current;if(!e)return;let t=e.querySelectorAll(".slider__item"),r=[];return t.forEach((o,h)=>{let c=o.querySelector("video");if(c)if(ne)c.pause(),c.style.visibility="hidden",o.classList.remove("slider__item--video-ready");else if(c.style.visibility="",h===L){c.currentTime=0,c.play();let u=()=>o.classList.add("slider__item--video-ready");c.readyState>=2?u():(c.addEventListener("canplay",u,{once:true}),r.push(()=>c.removeEventListener("canplay",u)));}else c.pause(),o.classList.remove("slider__item--video-ready");}),()=>r.forEach(o=>o())},[L,ne]);let g=useCallback(e=>{let t=e?.fromDrag===true;if(K.current)return;let r=d.current?.querySelectorAll(".slider__item");if(!r?.[L]){Z(false);return}let o=document.querySelector(".lightbox");o&&!t&&o.classList.add("lightbox--closing");let h=y.isMobile?"center":"start",c=d.current,u=()=>{let w=r[L];if(!w)return;let M=w.getBoundingClientRect(),C=h==="center"?window.innerWidth/2:y.inset,P=h==="center"?M.left+M.width/2:M.left;Math.abs(P-C)<4||w.scrollIntoView({behavior:"instant",block:"nearest",inline:h});};c?.addEventListener("scroll",u);let m=()=>c?.removeEventListener("scroll",u),S=()=>{if(K.current){m();return}if(document.startViewTransition){r.forEach(M=>{let C=M.querySelector("video");C&&(C.style.visibility="");}),document.documentElement.style.viewTransitionName="none";let w=document.startViewTransition(()=>{let M=document.querySelector(".lightbox__item--active");M&&(M.style.viewTransitionName="none");let C=document.querySelector(".lightbox");C&&(C.style.display="none"),u(),r[L].style.viewTransitionName="slider-active",flushSync(()=>Z(false));});K.current=w,w.finished.then(()=>{K.current=null,r[L].style.viewTransitionName="",document.documentElement.style.viewTransitionName="",m();});}else u(),Z(false),setTimeout(m,400);};t?requestAnimationFrame(S):o?setTimeout(S,350):S();},[L,y.isMobile]),f=useCallback(e=>{se(e),le.current=true,J(e),clearTimeout(B.current),B.current=setTimeout(()=>{le.current=false;},300);},[J]),p=s[L];return jsxs(motion.div,{className:`slider${D?" slider--morph-cursor":""}`,ref:V,variants:Xe,onPointerEnter:()=>W.current=true,onPointerLeave:()=>W.current=false,children:[jsx(motion.div,{className:"slider__track",ref:d,tabIndex:-1,onPointerDown:A,onPointerMove:v,onPointerUp:b,onPointerCancel:b,style:{paddingLeft:`${y.inset}px`,paddingRight:`${y.endPad}px`,scrollPaddingLeft:y.isMobile?void 0:`${y.inset}px`,touchAction:"pan-x pan-y"},children:s.map((e,t)=>{let r=e.id??t;return jsx(motion.div,{className:`slider__item${t===L?" slider__item--active":""}`,variants:_e,role:"button",tabIndex:0,"aria-label":e.title,style:{"--item-max-w":`${y.itemWidth}px`,"--item-max-h":`${R}px`,cursor:D?"none":"zoom-in"},onClick:()=>{re.current<5&&_(t);},onKeyDown:o=>{(o.key==="Enter"||o.key===" ")&&(o.preventDefault(),_(t));},children:jsxs("div",{className:"slider__item-inner",children:[jsxs("picture",{children:[e.webpSrcSet&&jsx("source",{srcSet:e.webpSrcSet,sizes:x,type:"image/webp"}),jsx("img",{src:e.src,srcSet:e.srcSet,sizes:x,alt:e.alt||e.title,draggable:false,fetchPriority:t===0?"high":void 0,loading:t<=1?"eager":"lazy",decoding:t<=1?"sync":"async",style:e.blurDataURL?{backgroundImage:`url(${e.blurDataURL})`,backgroundSize:"cover",backgroundRepeat:"no-repeat",backgroundPosition:"center"}:void 0})]}),e.video&&jsx("video",{src:e.video,muted:true,playsInline:true,loop:true,preload:"none",draggable:false})]})},r)})}),jsx(motion.div,{className:"slider__meta",style:{paddingLeft:`${y.metaInset}px`,paddingRight:`${y.metaInsetRight}px`},variants:_e,children:jsxs("div",{className:"slider__meta-inner",children:[jsx(a,{as:"h3",children:p?.title}),jsx("br",{}),jsx(a,{as:"p",children:p?.meta})]})}),jsx(AnimatePresence,{children:Y&&jsx(he,{items:s,activeIndex:L,sizes:F,Caption:a,controls:$,onActiveIndexChange:f,onClose:g})}),D&&jsx(we,{lightboxOpen:Y})]})}
2
+ export{he as Lightbox,Pe as PlainCaption,Ne as Slider};
@@ -0,0 +1 @@
1
+ :root{--slider-cursor-zoom-in: url(data:image/svg+xml,%3Csvg%20width%3D%2217%22%20height%3D%2217%22%20viewBox%3D%220%200%2017%2017%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Cpath%20d%3D%22M9.19643%207.80357H15V9.19643H9.19643V15H7.80357V9.19643H2V7.80357H7.80357V2H9.19643V7.80357Z%22%20fill%3D%22black%22%20stroke%3D%22white%22%20stroke-width%3D%220.3%22%20stroke-linejoin%3D%22round%22%2F%3E%0A%3C%2Fsvg%3E) 8 8, zoom-in}.slider{width:100vw;margin-top:3lh;margin-bottom:4.75lh;margin-left:calc(-50vw + 50%);@media(min-width:700px){margin-top:4lh;margin-bottom:6.75lh}.slider__track{display:flex;align-items:flex-end;gap:var(--gap);overflow-x:auto;@media(max-width:699px){gap:1.6rem}overflow-y:clip;scroll-snap-type:x mandatory;-webkit-overflow-scrolling:touch;scrollbar-width:none;padding-block:48px;margin-block:-48px;&::-webkit-scrollbar{display:none}&:focus{outline:none}}.slider__item{flex-shrink:0;width:fit-content;max-width:var(--item-max-w);scroll-snap-align:start;&:focus-visible{outline:2px solid var(--accent-color);outline-offset:4px;border-radius:18px}@media(max-width:699px){scroll-snap-align:center;width:var(--item-max-w);max-width:none}.slider__item-inner{position:relative;border-radius:16px;overflow:hidden;filter:none;transition:translate .35s cubic-bezier(.2,0,0,1),transform .35s cubic-bezier(.2,0,0,1),filter .35s cubic-bezier(.2,0,0,1);@media(max-width:699px){transition:none;border-radius:12px;will-change:transform,filter}@media(hover:hover){&:hover{translate:0 -6px;transform:scale(1.01);filter:drop-shadow(0 4px 10px rgba(0,0,0,.05)) drop-shadow(0 2px 4px rgba(0,0,0,.02))}}}&.slider__item--transitioning .slider__item-inner{filter:none;transition:none}img,video{display:block;border-radius:16px;cursor:var(--slider-cursor-zoom-in);width:auto;height:auto;max-width:var(--item-max-w);max-height:var(--item-max-h);@media(max-width:699px){width:100%;height:auto;max-width:none;max-height:none}}video{position:absolute;inset:0;width:100%;height:100%;max-width:none;max-height:none;object-fit:cover;opacity:0;transition:opacity .3s ease}&.slider__item--active video{opacity:1}&.slider__item--video-ready img{opacity:0}}.slider__meta{margin-top:.75lh;h3{color:var(--accent-color);margin:0}p{color:var(--text-color)}}}:root{--lightbox-cursor-zoom-out: url(data:image/svg+xml,%3Csvg%20width%3D%2217%22%20height%3D%2217%22%20viewBox%3D%220%200%2017%2017%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Cpath%20d%3D%22M9.54074%208.48008L13.9602%2012.8995L12.8995%2013.9602L8.48008%209.54074L4.06066%2013.9602L3%2012.8995L7.41942%208.48008L3%204.06066L4.06066%203L8.48008%207.41942L12.8995%203L13.9602%204.06066L9.54074%208.48008Z%22%20fill%3D%22black%22%20stroke%3D%22white%22%20stroke-width%3D%220.3%22%20stroke-linejoin%3D%22round%22%2F%3E%0A%3C%2Fsvg%3E) 8 8, zoom-out}.lightbox{position:fixed;inset:0;z-index:100;display:flex;align-items:center;--lb-caption-space: 8rem;--lb-controls-space: 0rem;padding-bottom:calc(var(--lb-caption-space) + var(--lb-controls-space));--lb-max-width: 95vw;--lb-max-height: calc( 100dvh - 96px - var(--lb-caption-space) - var(--lb-controls-space) );--lb-gap: 1.6rem;--lb-peek: 2.4rem;--lb-item-width: calc(100vw - 2 * (var(--lb-gap) + var(--lb-peek)));@media(min-width:700px){--lb-caption-space: 9.6rem;--lb-max-width: calc(100vw - 80px) ;--lb-max-height: calc( 100dvh - 40px - var(--lb-caption-space) - var(--lb-controls-space) )}&.lightbox--controls{--lb-controls-space: 6.4rem}.lightbox__backdrop{position:absolute;inset:0;background-color:#0000001a;backdrop-filter:blur(20px);cursor:var(--lightbox-cursor-zoom-out)}.lightbox__track{position:relative;display:flex;align-items:flex-end;gap:6.4rem;cursor:var(--lightbox-cursor-zoom-out);@media(max-width:699px){gap:var(--lb-gap)}overflow-x:auto;overflow-y:hidden;scroll-snap-type:x mandatory;-webkit-overflow-scrolling:touch;scrollbar-width:none;width:100%;padding-inline:50%;&::-webkit-scrollbar{display:none}}.lightbox__item{flex-shrink:0;width:fit-content;scroll-snap-align:center;position:relative;cursor:pointer;@media(max-width:699px){width:var(--lb-item-width)}}&:not(.lightbox--revealed) .lightbox__item:not(.lightbox__item--active){opacity:0;filter:blur(4px);transform:scale(.92);transition:opacity 1s cubic-bezier(0,.55,.45,1),transform 1s cubic-bezier(0,.55,.45,1),filter 1s cubic-bezier(0,.55,.45,1)}.lightbox__item img,.lightbox__item video{display:block;width:auto;height:auto;max-width:var(--lb-max-width);max-height:var(--lb-max-height);border-radius:12px;user-select:none;-webkit-user-drag:none;@media screen{border-radius:16px}@media(max-width:699px){width:100%;height:auto;max-width:none;max-height:var(--lb-max-height);object-fit:cover}}.lightbox__img{display:block}.lightbox__img--high{position:absolute;inset:0;opacity:0;transition:opacity .4s ease}.lightbox__img--high.lightbox__img--ready{opacity:1}.lightbox__img--high img{width:100%;height:100%;max-width:none;max-height:none;object-fit:cover}.lightbox__item video{position:absolute;inset:0;width:100%;height:100%;max-width:none;max-height:none;object-fit:cover;opacity:0}.lightbox__item--active{cursor:var(--lightbox-cursor-zoom-out);video{opacity:1}}.lightbox__item--video-ready img{opacity:0}.lightbox__meta{position:fixed;left:0;right:0;bottom:var(--lb-controls-space);height:var(--lb-caption-space);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.2rem;padding:0 2.4rem;text-align:center;pointer-events:none;opacity:0;transition:opacity 1s cubic-bezier(0,.55,.45,1);h2{margin:0;color:var(--accent-color)}p{margin:0;color:var(--text-color)}}.lightbox__controls{display:flex;gap:.8rem;position:fixed;bottom:1.6rem;left:50%;transform:translate(-50%);z-index:10;opacity:0;transition:opacity 1s cubic-bezier(0,.55,.45,1)}.lightbox__close,.lightbox__nav{display:flex;align-items:center;justify-content:center;width:4.4rem;height:4.4rem;padding:0;border:none;border-radius:12px;color:var(--color-text);cursor:pointer;background:color-mix(in srgb,var(--accent-color) 10%,transparent);backdrop-filter:blur(12px)}.lightbox__nav:disabled{opacity:.35;cursor:default}&.lightbox--revealed .lightbox__item{opacity:1;filter:blur(0px);transition:filter .4s cubic-bezier(.25,.46,.45,.94),transform .4s cubic-bezier(.25,.46,.45,.94);video{transition:opacity .3s ease}}&.lightbox--revealed .lightbox__item--active{transform:scale(1)}&.lightbox--revealed .lightbox__controls,&.lightbox--revealed .lightbox__meta{opacity:1}&.lightbox--closing .lightbox__backdrop{opacity:0;transition:opacity .3s cubic-bezier(0,.55,.45,1)}&.lightbox--closing .lightbox__item:not(.lightbox__item--active){opacity:0;filter:blur(4px);transform:scale(.92);transition:opacity .3s cubic-bezier(0,.55,.45,1),filter .3s cubic-bezier(0,.55,.45,1),transform .3s cubic-bezier(0,.55,.45,1)}&.lightbox--closing .lightbox__controls,&.lightbox--closing .lightbox__meta{opacity:0;transition:opacity .3s cubic-bezier(0,.55,.45,1)}&.lightbox--closing .lightbox__item--active{opacity:0;transition:opacity 0s linear .4s}}.lightbox__item--active{view-transition-name:slider-active}::view-transition-group(slider-active){animation-duration:.6s;animation-timing-function:cubic-bezier(.4,0,0,1)}::view-transition-old(slider-active),::view-transition-new(slider-active){animation-duration:.6s;animation-timing-function:cubic-bezier(.4,0,0,1)}.morph-cursor{position:fixed;top:0;left:0;z-index:2147483647;pointer-events:none;line-height:0;color:#fff;opacity:0;transition:opacity .15s ease;will-change:transform;filter:drop-shadow(0 0 1px rgba(0,0,0,.9)) drop-shadow(0 0 1px rgba(0,0,0,.6))}.morph-cursor--visible{opacity:1}.slider--morph-cursor .slider__item,.slider--morph-cursor .slider__item img,.slider--morph-cursor .slider__item video,.slider--morph-cursor .lightbox__backdrop,.slider--morph-cursor .lightbox__track,.slider--morph-cursor .lightbox__item,.slider--morph-cursor .lightbox__item--active{cursor:none}
package/index.d.ts ADDED
@@ -0,0 +1,88 @@
1
+ import type { ComponentType, ElementType, ReactNode } from "react";
2
+
3
+ /** A single panel in the slider / lightbox. All image fields are plain URLs. */
4
+ export interface SliderItem {
5
+ /** Unique key (falls back to the array index). */
6
+ id?: string | number;
7
+ /** Shown in the meta line. */
8
+ title?: string;
9
+ /** Secondary meta line, e.g. "Brand · 2024". */
10
+ meta?: string;
11
+ /** Required: featured image URL. */
12
+ src: string;
13
+ /** Responsive srcset for the panel image. */
14
+ srcSet?: string;
15
+ /** webp `<source>` srcset for the panel image. */
16
+ webpSrcSet?: string;
17
+ /** Low-quality placeholder (data URI). */
18
+ blurDataURL?: string;
19
+ /** Alt text (defaults to `title`). */
20
+ alt?: string;
21
+ /** Hi-res image for the lightbox (falls back to `src`). */
22
+ highResSrc?: string;
23
+ highResSrcSet?: string;
24
+ highResWebpSrcSet?: string;
25
+ /** Looping muted video URL; autoplays only while the panel is active. */
26
+ video?: string;
27
+ }
28
+
29
+ /** Props passed to the `Caption` component (matching metamorphosis's `TextMorph`). */
30
+ export interface CaptionProps {
31
+ as?: ElementType;
32
+ children?: ReactNode;
33
+ }
34
+
35
+ export interface SliderProps {
36
+ /** The panels. */
37
+ items: SliderItem[];
38
+ /** Desktop width (px) of the active panel's content column. Default `628`. */
39
+ contentWidth?: number;
40
+ /** Desktop gap (px) between panels. Default `32`. */
41
+ gap?: number;
42
+ /** Notional grid columns — only used to align the meta text. Default `4`. */
43
+ columns?: number;
44
+ /** Shift the meta text right by N columns on desktop (0 = flush). Default `0`. */
45
+ metaOffsetColumns?: number;
46
+ /** Minimum viewport margin (px/side) the content column keeps. Default `24`. */
47
+ sideMargin?: number;
48
+ /** Max height (px) of a panel; taller images scale down keeping ratio. Default `520`. */
49
+ maxItemHeight?: number;
50
+ /** `sizes` hint for the panel `<img>`. */
51
+ sizes?: string;
52
+ /** `sizes` hint forwarded to the lightbox images. Default `"84vw"`. */
53
+ lightboxSizes?: string;
54
+ /**
55
+ * Component used to render the meta title/subtitle. Receives `as` and
56
+ * `children`. Defaults to metamorphosis's morphing `TextMorph`; pass
57
+ * `PlainCaption` (or your own) to opt out of the animation.
58
+ */
59
+ Caption?: ComponentType<CaptionProps>;
60
+ /**
61
+ * Replace the static SVG cursors with a morphing icon cursor that follows the
62
+ * pointer (metamorphosis `IconMorph`): `+` over a slider panel, `×` over the
63
+ * active lightbox item or overlay, and `←`/`→` over the previous/next items.
64
+ * Precise-pointer only; touch and no-JS keep the static cursors. Default
65
+ * `false`.
66
+ */
67
+ morphCursor?: boolean;
68
+ }
69
+
70
+ export declare const Slider: ComponentType<SliderProps>;
71
+
72
+ /** Bare caption renderer (no animation). Pass as `Caption` to opt out of morphing. */
73
+ export declare const PlainCaption: ComponentType<CaptionProps>;
74
+
75
+ export interface LightboxProps {
76
+ /** Same item array passed to `<Slider>`. */
77
+ items: SliderItem[];
78
+ /** Index to open on. */
79
+ activeIndex: number;
80
+ /** `sizes` hint for the images. Default `"84vw"`. */
81
+ sizes?: string;
82
+ /** Called with the new index as the user scrolls. */
83
+ onActiveIndexChange?: (index: number) => void;
84
+ /** Called to dismiss the lightbox. */
85
+ onClose?: () => void;
86
+ }
87
+
88
+ export declare const Lightbox: ComponentType<LightboxProps>;
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@ocarignan/vitrine",
3
+ "version": "0.1.0",
4
+ "description": "A draggable project slider with a shared-element zoom lightbox for React. Self-contained, prop-driven, installable from npm.",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "main": "dist/index.js",
9
+ "module": "dist/index.mjs",
10
+ "types": "index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./index.d.ts",
14
+ "import": "./dist/index.mjs",
15
+ "require": "./dist/index.js"
16
+ },
17
+ "./styles.css": "./dist/styles.css"
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "index.d.ts"
22
+ ],
23
+ "sideEffects": [
24
+ "**/*.css"
25
+ ],
26
+ "keywords": [
27
+ "react",
28
+ "slider",
29
+ "carousel",
30
+ "lightbox",
31
+ "gallery",
32
+ "view-transitions"
33
+ ],
34
+ "scripts": {
35
+ "build": "tsup",
36
+ "dev": "tsup --watch",
37
+ "prepare": "tsup"
38
+ },
39
+ "peerDependencies": {
40
+ "react": ">=18",
41
+ "react-dom": ">=18"
42
+ },
43
+ "dependencies": {
44
+ "@ocarignan/metamorphosis": "^0.1.0",
45
+ "motion": "^12.19.1"
46
+ },
47
+ "devDependencies": {
48
+ "tsup": "^8.5.0",
49
+ "typescript": "^5.9.3"
50
+ },
51
+ "repository": {
52
+ "type": "git",
53
+ "url": "git+https://github.com/olicarignan/vitrine.git"
54
+ }
55
+ }