@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 +215 -0
- package/dist/index.js +2 -0
- package/dist/index.mjs +2 -0
- package/dist/styles.css +1 -0
- package/index.d.ts +88 -0
- package/package.json +55 -0
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};
|
package/dist/styles.css
ADDED
|
@@ -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
|
+
}
|