apex-ui-kit 2.6.0 → 2.8.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 +32 -10
- package/dist/ApexUI-Kit/templates/AccordionMarquee/AccordionMarquee.jsx +340 -161
- package/dist/ApexUI-Kit/templates/AnimatedTabs/AnimatedTabs.jsx +1 -1
- package/dist/ApexUI-Kit/templates/ArcGallery/ArcGallery.jsx +356 -0
- package/dist/ApexUI-Kit/templates/BounceCards/BounceCards.jsx +22 -2
- package/dist/ApexUI-Kit/templates/CircularCarousel/CircularCarousel.jsx +381 -0
- package/dist/ApexUI-Kit/templates/CommandPalette/CommandPalette.jsx +250 -0
- package/dist/ApexUI-Kit/templates/CrossedMarquee/CrossedMarquee.jsx +158 -0
- package/dist/ApexUI-Kit/templates/DitherWaves/DitherWaves.jsx +207 -0
- package/dist/ApexUI-Kit/templates/EdgeBlur/EdgeBlur.jsx +92 -0
- package/dist/ApexUI-Kit/templates/FlipClock/FlipClock.jsx +5 -1
- package/dist/ApexUI-Kit/templates/GlassLens/GlassLens.jsx +274 -0
- package/dist/ApexUI-Kit/templates/GooeyNav/GooeyNav.jsx +2 -2
- package/dist/ApexUI-Kit/templates/HorizonGlow/HorizonGlow.jsx +259 -0
- package/dist/ApexUI-Kit/templates/ImageAccordion/ImageAccordion.jsx +116 -0
- package/dist/ApexUI-Kit/templates/InfiniteCanvas/InfiniteCanvas.jsx +230 -0
- package/dist/ApexUI-Kit/templates/MagneticButton/MagneticButton.jsx +69 -0
- package/dist/ApexUI-Kit/templates/Marquee3D/Marquee3D.jsx +68 -0
- package/dist/ApexUI-Kit/templates/NavDropDown/NavDropDown.jsx +123 -1
- package/dist/ApexUI-Kit/templates/OrbitIcons/OrbitIcons.jsx +169 -0
- package/dist/ApexUI-Kit/templates/ParticleText/ParticleText.jsx +328 -0
- package/dist/ApexUI-Kit/templates/RadialMenu/RadialMenu.jsx +119 -0
- package/dist/ApexUI-Kit/templates/ScrollStack/ScrollStack.jsx +88 -0
- package/dist/ApexUI-Kit/templates/SidebarNav/SidebarNav.jsx +22 -1
- package/dist/ApexUI-Kit/templates/SunriseBackground/SunriseBackground.jsx +290 -0
- package/dist/ApexUI-Kit/templates/TextRing3D/TextRing3D.jsx +18 -3
- package/dist/ApexUI-Kit/templates/ThemeSwitcher/ThemeSwitcher.jsx +215 -0
- package/dist/ApexUI-Kit/templates/ThemeSwitcher/themes.js +38 -0
- package/dist/ApexUI-Kit/templates/ToastStack/ToastStack.jsx +21 -13
- package/dist/ApexUI-Kit/templates/WireframeKnot3D/WireframeKnot3D.jsx +112 -0
- package/dist/cli/index.js +20 -0
- package/package.json +2 -2
- package/dist/ApexUI-Kit/templates/AccordionMarquee/AccordionMarquee.css +0 -1
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { Mesh, OrthographicCamera, PlaneGeometry, Scene, ShaderMaterial, SRGBColorSpace, TextureLoader, Vector2, WebGLRenderer } from "three";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Arc Gallery: an endless row of image cards laid along a gentle arc and
|
|
6
|
+
* drawn in WebGL. Drag, flick or scroll the wheel to move it; it eases to a
|
|
7
|
+
* stop and settles on a card. While it moves the cards ripple slightly,
|
|
8
|
+
* the edges fade out, and a title sits under each card. Arrow keys step.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const IK = "https://ik.imagekit.io/ny6yinyut/apexui/assets";
|
|
12
|
+
const img = (n) => `${IK}/${n}.png?tr=f-auto,q-80,w-640`;
|
|
13
|
+
const DEFAULT_ITEMS = [
|
|
14
|
+
{ image: img("spiderman"), text: "Spider-Man" },
|
|
15
|
+
{ image: img("black-panther"), text: "Black Panther" },
|
|
16
|
+
{ image: img("hulk"), text: "Hulk" },
|
|
17
|
+
{ image: img("thanos"), text: "Thanos" },
|
|
18
|
+
{ image: img("captainamerica"), text: "Captain America" },
|
|
19
|
+
{ image: img("doctorStrange"), text: "Doctor Strange" },
|
|
20
|
+
{ image: img("ironman"), text: "Iron Man" },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const VERT = `
|
|
24
|
+
uniform float uWave; uniform float uTime; uniform float uPull;
|
|
25
|
+
varying vec2 vUv;
|
|
26
|
+
void main(){
|
|
27
|
+
vUv = uv;
|
|
28
|
+
vec3 p = position;
|
|
29
|
+
// A soft ripple that only shows while the gallery moves.
|
|
30
|
+
p.y += sin(uv.x * 3.14159 * 2.0 + uTime * 4.0) * uWave;
|
|
31
|
+
// Pulled along: the middle of the card lags behind its corners, so the
|
|
32
|
+
// trailing edge bows out into a half circle while it moves.
|
|
33
|
+
float t = uv.y * 2.0 - 1.0;
|
|
34
|
+
float bow = sqrt(max(0.0, 1.0 - t * t));
|
|
35
|
+
float trail = uPull > 0.0 ? uv.x : 1.0 - uv.x;
|
|
36
|
+
p.x += uPull * bow * mix(0.25, 1.0, trail);
|
|
37
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0);
|
|
38
|
+
}`;
|
|
39
|
+
|
|
40
|
+
const FRAG = `
|
|
41
|
+
uniform sampler2D uTex; uniform float uReady; uniform vec2 uCover; uniform float uCorner; uniform float uAspect;
|
|
42
|
+
uniform float uFade; uniform float uViewW; uniform float uScreenX;
|
|
43
|
+
varying vec2 vUv;
|
|
44
|
+
void main(){
|
|
45
|
+
vec2 q = abs(vUv - 0.5) * vec2(uAspect, 1.0);
|
|
46
|
+
vec2 b = vec2(uAspect, 1.0) * 0.5 - uCorner;
|
|
47
|
+
float d = length(max(q - b, 0.0)) - uCorner;
|
|
48
|
+
float edge = 1.0 - smoothstep(-0.004, 0.004, d);
|
|
49
|
+
if (edge <= 0.0) discard;
|
|
50
|
+
vec3 col = texture2D(uTex, (vUv - 0.5) * uCover + 0.5).rgb;
|
|
51
|
+
// Fade toward the left and right edges of the view.
|
|
52
|
+
float u = clamp(gl_FragCoord.x / uViewW, 0.0, 1.0);
|
|
53
|
+
float f = uFade > 0.0 ? smoothstep(0.0, uFade, u) * smoothstep(0.0, uFade, 1.0 - u) : 1.0;
|
|
54
|
+
gl_FragColor = vec4(col, edge * f * uReady);
|
|
55
|
+
}`;
|
|
56
|
+
|
|
57
|
+
const ArcGallery = ({ items = DEFAULT_ITEMS, bend = 1, cardHeight = 0.62, aspectRatio = 0.78, gap = 0.22, cornerRadius = 0.05, wave = 1, pull = 1, fadeEdges = 0.14, ease = 0.08, snap = true, showTitles = true, height = "26rem", onChange, className = "" }) => {
|
|
58
|
+
const wrap = useRef(null);
|
|
59
|
+
const labels = useRef([]);
|
|
60
|
+
const api = useRef(null);
|
|
61
|
+
const [active, setActive] = useState(0);
|
|
62
|
+
const live = useRef({});
|
|
63
|
+
live.current = { bend, wave, pull, fadeEdges, ease, snap, onChange };
|
|
64
|
+
const list = items.length ? items : DEFAULT_ITEMS;
|
|
65
|
+
const key = list.map((i) => i.image).join("|");
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
const el = wrap.current;
|
|
69
|
+
const n = list.length;
|
|
70
|
+
const renderer = new WebGLRenderer({ antialias: true, alpha: true, powerPreference: "high-performance" });
|
|
71
|
+
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
72
|
+
renderer.setPixelRatio(dpr);
|
|
73
|
+
renderer.outputColorSpace = SRGBColorSpace;
|
|
74
|
+
Object.assign(renderer.domElement.style, { display: "block", width: "100%", height: "100%", touchAction: "pan-y" });
|
|
75
|
+
el.prepend(renderer.domElement);
|
|
76
|
+
const scene = new Scene();
|
|
77
|
+
// Orthographic camera in CSS pixels, origin at the center.
|
|
78
|
+
const camera = new OrthographicCamera(-1, 1, 1, -1, -1000, 1000);
|
|
79
|
+
const geometry = new PlaneGeometry(1, 1, 24, 24);
|
|
80
|
+
const loader = new TextureLoader().setCrossOrigin("anonymous");
|
|
81
|
+
const view = { w: 1, h: 1 };
|
|
82
|
+
// Enough meshes to cover the view plus a buffer, recycled as the row moves.
|
|
83
|
+
const slots = [];
|
|
84
|
+
const textures = list.map((item) => {
|
|
85
|
+
const entry = { tex: null, aspect: 1, at: 0 };
|
|
86
|
+
loader.load(item.image, (t) => {
|
|
87
|
+
t.colorSpace = SRGBColorSpace;
|
|
88
|
+
entry.tex = t;
|
|
89
|
+
entry.aspect = t.image.width / t.image.height;
|
|
90
|
+
entry.at = performance.now();
|
|
91
|
+
wake();
|
|
92
|
+
});
|
|
93
|
+
return entry;
|
|
94
|
+
});
|
|
95
|
+
const makeSlot = () => {
|
|
96
|
+
const m = new ShaderMaterial({
|
|
97
|
+
vertexShader: VERT,
|
|
98
|
+
fragmentShader: FRAG,
|
|
99
|
+
transparent: true,
|
|
100
|
+
uniforms: {
|
|
101
|
+
uTex: { value: null },
|
|
102
|
+
uReady: { value: 0 },
|
|
103
|
+
uCover: { value: new Vector2(1, 1) },
|
|
104
|
+
uCorner: { value: cornerRadius },
|
|
105
|
+
uAspect: { value: aspectRatio },
|
|
106
|
+
uFade: { value: fadeEdges },
|
|
107
|
+
uViewW: { value: 1 },
|
|
108
|
+
uScreenX: { value: 0 },
|
|
109
|
+
uWave: { value: 0 },
|
|
110
|
+
uTime: { value: 0 },
|
|
111
|
+
uPull: { value: 0 },
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
const mesh = new Mesh(geometry, m);
|
|
115
|
+
scene.add(mesh);
|
|
116
|
+
return { mesh, m };
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const s = { scroll: 0, target: 0, vel: 0, pull: 0, drag: null, time: 0 };
|
|
120
|
+
let cardW = 1;
|
|
121
|
+
let cardH = 1;
|
|
122
|
+
let spacing = 1;
|
|
123
|
+
let raf = 0;
|
|
124
|
+
let last = 0;
|
|
125
|
+
let visible = true;
|
|
126
|
+
let shown = -1;
|
|
127
|
+
let snapTimer = 0;
|
|
128
|
+
|
|
129
|
+
const size = () => {
|
|
130
|
+
const { width, height: hh } = el.getBoundingClientRect();
|
|
131
|
+
if (!width || !hh) return;
|
|
132
|
+
view.w = width;
|
|
133
|
+
view.h = hh;
|
|
134
|
+
renderer.setSize(width, hh, false);
|
|
135
|
+
camera.left = -width / 2;
|
|
136
|
+
camera.right = width / 2;
|
|
137
|
+
camera.top = hh / 2;
|
|
138
|
+
camera.bottom = -hh / 2;
|
|
139
|
+
camera.updateProjectionMatrix();
|
|
140
|
+
const old = spacing;
|
|
141
|
+
cardH = hh * cardHeight;
|
|
142
|
+
// At least two and a bit cards across, even in a tall narrow box.
|
|
143
|
+
cardW = Math.min(cardH * aspectRatio, Math.max(width * 0.36, Math.min(width * 0.62, 200)));
|
|
144
|
+
cardH = cardW / aspectRatio;
|
|
145
|
+
spacing = cardW * (1 + gap);
|
|
146
|
+
if (old > 1) {
|
|
147
|
+
s.scroll = (s.scroll / old) * spacing;
|
|
148
|
+
s.target = (s.target / old) * spacing;
|
|
149
|
+
}
|
|
150
|
+
const need = Math.ceil(width / spacing) + 4;
|
|
151
|
+
while (slots.length < need) slots.push(makeSlot());
|
|
152
|
+
wake();
|
|
153
|
+
};
|
|
154
|
+
const ro = new ResizeObserver(size);
|
|
155
|
+
ro.observe(el);
|
|
156
|
+
|
|
157
|
+
// Point on the arc for a horizontal offset x (circle through the edges).
|
|
158
|
+
const arc = (x) => {
|
|
159
|
+
const b = live.current.bend;
|
|
160
|
+
if (!b) return { y: 0, rot: 0 };
|
|
161
|
+
const depth = Math.min(view.w, view.h * 2) * 0.05 * Math.abs(b);
|
|
162
|
+
const half = view.w / 2 + spacing;
|
|
163
|
+
const r = (half * half + depth * depth) / (2 * depth);
|
|
164
|
+
const reach = Math.min(Math.abs(x), r * 0.999);
|
|
165
|
+
const y = r - Math.sqrt(r * r - reach * reach);
|
|
166
|
+
const rot = Math.asin(reach / r) * Math.sign(x);
|
|
167
|
+
return b > 0 ? { y: -y, rot: -rot } : { y, rot };
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const frame = (now) => {
|
|
171
|
+
raf = 0;
|
|
172
|
+
const o = live.current;
|
|
173
|
+
const dt = last ? Math.min(0.05, (now - last) / 1000) : 1 / 60;
|
|
174
|
+
last = now;
|
|
175
|
+
const before = s.scroll;
|
|
176
|
+
const follow = 1 - Math.pow(1 - Math.min(1, Math.max(0.01, o.ease)), dt * 60);
|
|
177
|
+
s.scroll += (s.target - s.scroll) * follow;
|
|
178
|
+
if (Math.abs(s.target - s.scroll) < 0.05) s.scroll = s.target;
|
|
179
|
+
const v = (s.scroll - before) / dt;
|
|
180
|
+
s.vel += (v - s.vel) * (1 - Math.exp(-dt / 0.08));
|
|
181
|
+
if (Math.abs(s.vel) < 1) s.vel = 0;
|
|
182
|
+
s.time += dt;
|
|
183
|
+
const waveAmt = o.wave * Math.min(1, Math.abs(s.vel) / (spacing * 6)) * cardH * 0.05;
|
|
184
|
+
// Bow toward the side it came from (cards moving left lag to the right).
|
|
185
|
+
const pullGoal = Math.max(-1, Math.min(1, s.vel / (spacing * 7))) * 0.32 * o.pull;
|
|
186
|
+
s.pull += (pullGoal - s.pull) * (1 - Math.exp(-dt / 0.12));
|
|
187
|
+
if (Math.abs(s.pull) < 0.0005 && !pullGoal) s.pull = 0;
|
|
188
|
+
let busy = s.pull !== 0 || s.scroll !== s.target || s.vel !== 0 || !!s.drag;
|
|
189
|
+
|
|
190
|
+
const first = Math.floor((s.scroll - view.w / 2) / spacing) - 1;
|
|
191
|
+
slots.forEach((slot, k) => {
|
|
192
|
+
const index = first + k;
|
|
193
|
+
const x = index * spacing - s.scroll;
|
|
194
|
+
const item = ((index % n) + n) % n;
|
|
195
|
+
const entry = textures[item];
|
|
196
|
+
const { y, rot } = arc(x);
|
|
197
|
+
slot.mesh.position.set(x, y + cardH * 0.06, 0);
|
|
198
|
+
slot.mesh.rotation.z = rot;
|
|
199
|
+
slot.mesh.scale.set(cardW, cardH, 1);
|
|
200
|
+
const m = slot.m.uniforms;
|
|
201
|
+
m.uTex.value = entry.tex;
|
|
202
|
+
m.uReady.value = entry.at ? Math.min(1, (now - entry.at) / 400) : 0;
|
|
203
|
+
if (entry.at && m.uReady.value < 1) busy = true;
|
|
204
|
+
const ia = entry.aspect;
|
|
205
|
+
m.uCover.value.set(ia > aspectRatio ? aspectRatio / ia : 1, ia > aspectRatio ? 1 : ia / aspectRatio);
|
|
206
|
+
m.uFade.value = o.fadeEdges;
|
|
207
|
+
m.uViewW.value = view.w * dpr;
|
|
208
|
+
m.uWave.value = waveAmt / cardH;
|
|
209
|
+
m.uTime.value = s.time;
|
|
210
|
+
m.uPull.value = s.pull;
|
|
211
|
+
// Title under the card.
|
|
212
|
+
const label = labels.current[k];
|
|
213
|
+
if (label) {
|
|
214
|
+
if (showTitles) {
|
|
215
|
+
label.textContent = list[item].text ?? "";
|
|
216
|
+
const ly = -(y + cardH * 0.06) + cardH / 2 + 18;
|
|
217
|
+
const lx = x - Math.sin(rot) * (cardH / 2 + 18);
|
|
218
|
+
const u = (lx + view.w / 2) / view.w;
|
|
219
|
+
const f = o.fadeEdges > 0 ? Math.min(1, Math.max(0, Math.min(u, 1 - u) / o.fadeEdges)) : 1;
|
|
220
|
+
label.style.transform = `translate(${lx + view.w / 2}px, ${ly + view.h / 2}px) translate(-50%, -50%) rotate(${-rot}rad)`;
|
|
221
|
+
label.style.opacity = String(f);
|
|
222
|
+
} else label.style.opacity = "0";
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
renderer.render(scene, camera);
|
|
226
|
+
|
|
227
|
+
const idx = ((Math.round(s.scroll / spacing) % n) + n) % n;
|
|
228
|
+
if (idx !== shown) {
|
|
229
|
+
shown = idx;
|
|
230
|
+
setActive(idx);
|
|
231
|
+
o.onChange?.(idx);
|
|
232
|
+
}
|
|
233
|
+
if (busy && visible) raf = requestAnimationFrame(frame);
|
|
234
|
+
else last = 0;
|
|
235
|
+
};
|
|
236
|
+
function wake() {
|
|
237
|
+
if (!raf && visible) raf = requestAnimationFrame(frame);
|
|
238
|
+
}
|
|
239
|
+
const settle = () => {
|
|
240
|
+
if (live.current.snap) s.target = Math.round(s.target / spacing) * spacing;
|
|
241
|
+
wake();
|
|
242
|
+
};
|
|
243
|
+
api.current = {
|
|
244
|
+
step: (d) => {
|
|
245
|
+
s.target = (Math.round(s.target / spacing) + d) * spacing;
|
|
246
|
+
wake();
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const canvas = renderer.domElement;
|
|
251
|
+
const onDown = (e) => {
|
|
252
|
+
if (e.button !== 0) return;
|
|
253
|
+
s.drag = { x: e.clientX, start: s.target, moved: false, samples: [] };
|
|
254
|
+
};
|
|
255
|
+
const onMove = (e) => {
|
|
256
|
+
const g = s.drag;
|
|
257
|
+
if (!g) return;
|
|
258
|
+
const dx = e.clientX - g.x;
|
|
259
|
+
if (!g.moved && Math.abs(dx) > 5) {
|
|
260
|
+
g.moved = true;
|
|
261
|
+
canvas.setPointerCapture(e.pointerId);
|
|
262
|
+
}
|
|
263
|
+
if (!g.moved) return;
|
|
264
|
+
s.target = g.start - dx * 1.4;
|
|
265
|
+
g.samples.push({ x: e.clientX, t: performance.now() });
|
|
266
|
+
if (g.samples.length > 6) g.samples.shift();
|
|
267
|
+
wake();
|
|
268
|
+
};
|
|
269
|
+
const onUp = (e) => {
|
|
270
|
+
const g = s.drag;
|
|
271
|
+
s.drag = null;
|
|
272
|
+
if (!g) return;
|
|
273
|
+
if (g.moved && g.samples.length > 1) {
|
|
274
|
+
const a = g.samples[0];
|
|
275
|
+
const b = g.samples[g.samples.length - 1];
|
|
276
|
+
const v = ((b.x - a.x) / Math.max(16, b.t - a.t)) * 1000;
|
|
277
|
+
s.target -= v * 0.25;
|
|
278
|
+
} else if (!g.moved) {
|
|
279
|
+
// Click: bring the clicked card to the center.
|
|
280
|
+
const r = canvas.getBoundingClientRect();
|
|
281
|
+
const x = e.clientX - r.left - r.width / 2;
|
|
282
|
+
s.target = Math.round((s.scroll + x) / spacing) * spacing;
|
|
283
|
+
wake();
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
settle();
|
|
287
|
+
};
|
|
288
|
+
const onWheel = (e) => {
|
|
289
|
+
const d = Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : e.deltaY;
|
|
290
|
+
if (!d) return;
|
|
291
|
+
e.preventDefault();
|
|
292
|
+
s.target += d * 1.2;
|
|
293
|
+
clearTimeout(snapTimer);
|
|
294
|
+
snapTimer = setTimeout(settle, 140);
|
|
295
|
+
wake();
|
|
296
|
+
};
|
|
297
|
+
canvas.addEventListener("pointerdown", onDown);
|
|
298
|
+
canvas.addEventListener("pointermove", onMove);
|
|
299
|
+
canvas.addEventListener("pointerup", onUp);
|
|
300
|
+
canvas.addEventListener("pointercancel", onUp);
|
|
301
|
+
canvas.addEventListener("wheel", onWheel, { passive: false });
|
|
302
|
+
const io = new IntersectionObserver(([entry]) => {
|
|
303
|
+
visible = entry.isIntersecting;
|
|
304
|
+
wake();
|
|
305
|
+
});
|
|
306
|
+
io.observe(el);
|
|
307
|
+
size();
|
|
308
|
+
|
|
309
|
+
return () => {
|
|
310
|
+
cancelAnimationFrame(raf);
|
|
311
|
+
clearTimeout(snapTimer);
|
|
312
|
+
ro.disconnect();
|
|
313
|
+
io.disconnect();
|
|
314
|
+
canvas.removeEventListener("pointerdown", onDown);
|
|
315
|
+
canvas.removeEventListener("pointermove", onMove);
|
|
316
|
+
canvas.removeEventListener("pointerup", onUp);
|
|
317
|
+
canvas.removeEventListener("pointercancel", onUp);
|
|
318
|
+
canvas.removeEventListener("wheel", onWheel);
|
|
319
|
+
textures.forEach((t) => t.tex?.dispose());
|
|
320
|
+
slots.forEach((slot) => slot.m.dispose());
|
|
321
|
+
geometry.dispose();
|
|
322
|
+
renderer.dispose();
|
|
323
|
+
renderer.forceContextLoss();
|
|
324
|
+
canvas.remove();
|
|
325
|
+
api.current = null;
|
|
326
|
+
};
|
|
327
|
+
// Rebuild when the images or card shape change; motion settings are live.
|
|
328
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
329
|
+
}, [key, cardHeight, aspectRatio, gap, cornerRadius, showTitles]);
|
|
330
|
+
|
|
331
|
+
return (
|
|
332
|
+
<div
|
|
333
|
+
ref={wrap}
|
|
334
|
+
role="region"
|
|
335
|
+
aria-roledescription="carousel"
|
|
336
|
+
aria-label={`Image gallery, ${list[active]?.text ?? ""}`}
|
|
337
|
+
tabIndex={0}
|
|
338
|
+
onKeyDown={(e) => {
|
|
339
|
+
if (e.key === "ArrowRight") api.current?.step(1);
|
|
340
|
+
else if (e.key === "ArrowLeft") api.current?.step(-1);
|
|
341
|
+
else return;
|
|
342
|
+
e.preventDefault();
|
|
343
|
+
}}
|
|
344
|
+
className={`relative w-full cursor-grab select-none overflow-hidden outline-none active:cursor-grabbing focus-visible:ring-2 focus-visible:ring-white/30 ${className}`}
|
|
345
|
+
style={{ height }}
|
|
346
|
+
>
|
|
347
|
+
<div className="pointer-events-none absolute inset-0" aria-hidden="true">
|
|
348
|
+
{Array.from({ length: 24 }, (_, k) => (
|
|
349
|
+
<span key={k} ref={(e) => (labels.current[k] = e)} className="absolute left-0 top-0 whitespace-nowrap text-sm font-semibold opacity-0" />
|
|
350
|
+
))}
|
|
351
|
+
</div>
|
|
352
|
+
</div>
|
|
353
|
+
);
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
export default ArcGallery;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState } from "react";
|
|
1
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
2
2
|
import { motion } from "framer-motion";
|
|
3
3
|
|
|
4
4
|
/*
|
|
@@ -13,10 +13,29 @@ const DEFAULT_IMAGES = [img("hulk"), img("captainamerica"), img("spiderman"), im
|
|
|
13
13
|
|
|
14
14
|
const BounceCards = ({ images = DEFAULT_IMAGES, cardSize = 9, spread = 5.5, fan = 8, delay = 0.2, className = "" }) => {
|
|
15
15
|
const [hovered, setHovered] = useState(null);
|
|
16
|
+
const [scale, setScale] = useState(1);
|
|
17
|
+
const root = useRef(null);
|
|
16
18
|
const mid = (images.length - 1) / 2;
|
|
19
|
+
// Width the fan needs (in rem): cards plus spread plus room for the tilt.
|
|
20
|
+
const needed = cardSize + spread * (images.length - 1) + 4;
|
|
21
|
+
|
|
22
|
+
// Shrink the whole fan to fit narrow screens instead of cutting cards off.
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
const el = root.current;
|
|
25
|
+
if (!el) return undefined;
|
|
26
|
+
const fit = () => {
|
|
27
|
+
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
|
|
28
|
+
setScale(Math.min(1, el.clientWidth / (needed * rem)));
|
|
29
|
+
};
|
|
30
|
+
const ro = new ResizeObserver(fit);
|
|
31
|
+
ro.observe(el);
|
|
32
|
+
fit();
|
|
33
|
+
return () => ro.disconnect();
|
|
34
|
+
}, [needed]);
|
|
17
35
|
|
|
18
36
|
return (
|
|
19
|
-
<div className={`relative
|
|
37
|
+
<div ref={root} className={`relative ${className}`} style={{ width: `min(${needed}rem, 100%)`, height: `${cardSize * 1.5 * scale}rem` }} onPointerLeave={() => setHovered(null)}>
|
|
38
|
+
<div className="absolute left-1/2 top-1/2 flex items-center justify-center" style={{ width: `${needed}rem`, height: `${cardSize * 1.5}rem`, transform: `translate(-50%, -50%) scale(${scale})` }}>
|
|
20
39
|
{images.map((src, i) => {
|
|
21
40
|
const off = i - mid;
|
|
22
41
|
let x = off * spread;
|
|
@@ -46,6 +65,7 @@ const BounceCards = ({ images = DEFAULT_IMAGES, cardSize = 9, spread = 5.5, fan
|
|
|
46
65
|
</motion.div>
|
|
47
66
|
);
|
|
48
67
|
})}
|
|
68
|
+
</div>
|
|
49
69
|
</div>
|
|
50
70
|
);
|
|
51
71
|
};
|