apex-ui-kit 2.6.0 → 2.7.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 +22 -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/EdgeBlur/EdgeBlur.jsx +92 -0
- package/dist/ApexUI-Kit/templates/FlipClock/FlipClock.jsx +5 -1
- 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/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/ParticleText/ParticleText.jsx +328 -0
- package/dist/ApexUI-Kit/templates/RadialMenu/RadialMenu.jsx +103 -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/WireframeKnot3D/WireframeKnot3D.jsx +112 -0
- package/dist/cli/index.js +11 -0
- package/package.json +2 -2
- package/dist/ApexUI-Kit/templates/AccordionMarquee/AccordionMarquee.css +0 -1
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { Color, DoubleSide, Group, Mesh, PerspectiveCamera, PlaneGeometry, Scene, ShaderMaterial, SRGBColorSpace, TextureLoader, Vector2, Vector3, WebGLRenderer } from "three";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Circular Carousel: image cards bent around a cylinder and drawn in WebGL.
|
|
6
|
+
* The ring drifts on its own; drag or flick to throw it, and it glides,
|
|
7
|
+
* then settles on a card. Hover pauses, a click brings that card to the
|
|
8
|
+
* front, arrows / keys step through. Cards turning away fade into the
|
|
9
|
+
* background and you can see their backs through the ring.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const IK = "https://ik.imagekit.io/ny6yinyut/apexui/assets";
|
|
13
|
+
const img = (n) => `${IK}/${n}.png?tr=f-auto,q-80,w-640`;
|
|
14
|
+
const DEFAULT_ITEMS = [
|
|
15
|
+
{ src: img("spiderman"), title: "Spider-Man" },
|
|
16
|
+
{ src: img("black-panther"), title: "Black Panther" },
|
|
17
|
+
{ src: img("hulk"), title: "Hulk" },
|
|
18
|
+
{ src: img("thanos"), title: "Thanos" },
|
|
19
|
+
{ src: img("captainamerica"), title: "Captain America" },
|
|
20
|
+
{ src: img("doctorStrange"), title: "Doctor Strange" },
|
|
21
|
+
{ src: img("ironman"), title: "Iron Man" },
|
|
22
|
+
{ src: img("spiderman"), title: "Spider-Man" },
|
|
23
|
+
{ src: img("hulk"), title: "Hulk" },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const VERT = `
|
|
27
|
+
uniform float uRadius; uniform float uBase;
|
|
28
|
+
varying vec2 vUv; varying float vFacing;
|
|
29
|
+
void main(){
|
|
30
|
+
vUv = uv;
|
|
31
|
+
// Bend the flat card onto the cylinder.
|
|
32
|
+
float a = uBase + position.x / uRadius;
|
|
33
|
+
vec3 p = vec3(sin(a) * uRadius, position.y, cos(a) * uRadius);
|
|
34
|
+
vec3 n = vec3(sin(a), 0.0, cos(a));
|
|
35
|
+
vFacing = (modelViewMatrix * vec4(n, 0.0)).z;
|
|
36
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0);
|
|
37
|
+
}`;
|
|
38
|
+
|
|
39
|
+
const FRAG = `
|
|
40
|
+
uniform sampler2D uTex; uniform float uReady; uniform vec2 uCover; uniform float uCorner;
|
|
41
|
+
uniform vec3 uFade; uniform float uDepth; uniform float uAspect; uniform float uInner;
|
|
42
|
+
varying vec2 vUv; varying float vFacing;
|
|
43
|
+
void main(){
|
|
44
|
+
vec2 uv = gl_FrontFacing ? vUv : vec2(1.0 - vUv.x, vUv.y);
|
|
45
|
+
// Rounded corners.
|
|
46
|
+
vec2 q = abs(uv - 0.5) * vec2(uAspect, 1.0);
|
|
47
|
+
vec2 b = vec2(uAspect, 1.0) * 0.5 - uCorner;
|
|
48
|
+
float d = length(max(q - b, 0.0)) - uCorner;
|
|
49
|
+
float edge = 1.0 - smoothstep(-0.004, 0.004, d);
|
|
50
|
+
if (edge <= 0.0) discard;
|
|
51
|
+
vec3 col = texture2D(uTex, (uv - 0.5) * uCover + 0.5).rgb;
|
|
52
|
+
// Backs of far cards melt into the background, so it works on light and dark.
|
|
53
|
+
if (!gl_FrontFacing) col = mix(col, uFade, uInner);
|
|
54
|
+
float away = clamp((1.0 - vFacing) * 0.5, 0.0, 1.0);
|
|
55
|
+
col = mix(col, uFade, uDepth * pow(away, 1.2));
|
|
56
|
+
gl_FragColor = vec4(col, edge * uReady);
|
|
57
|
+
}`;
|
|
58
|
+
|
|
59
|
+
const CircularCarousel = ({
|
|
60
|
+
items = DEFAULT_ITEMS,
|
|
61
|
+
cardWidth = 1,
|
|
62
|
+
aspectRatio = 0.8,
|
|
63
|
+
gap = 0.12,
|
|
64
|
+
speed = 12,
|
|
65
|
+
autoplay = true,
|
|
66
|
+
pauseOnHover = true,
|
|
67
|
+
snap = true,
|
|
68
|
+
tilt = -6,
|
|
69
|
+
depthFade = 0.6,
|
|
70
|
+
fadeColor = "#0a0a0a",
|
|
71
|
+
cornerRadius = 0.06,
|
|
72
|
+
captions = true,
|
|
73
|
+
height = "28rem",
|
|
74
|
+
onChange,
|
|
75
|
+
className = "",
|
|
76
|
+
}) => {
|
|
77
|
+
const wrap = useRef(null);
|
|
78
|
+
const api = useRef(null);
|
|
79
|
+
const [active, setActive] = useState(0);
|
|
80
|
+
const live = useRef({});
|
|
81
|
+
live.current = { speed, autoplay, pauseOnHover, snap, tilt, depthFade, fadeColor, onChange };
|
|
82
|
+
const list = items.length ? items : DEFAULT_ITEMS;
|
|
83
|
+
const key = list.map((i) => i.src).join("|");
|
|
84
|
+
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
const el = wrap.current;
|
|
87
|
+
const n = list.length;
|
|
88
|
+
const step = (Math.PI * 2) / n;
|
|
89
|
+
const w = cardWidth;
|
|
90
|
+
const h = w / aspectRatio;
|
|
91
|
+
// Radius so the cards (plus gap) wrap the ring exactly.
|
|
92
|
+
const R = (n * w * (1 + gap)) / (Math.PI * 2);
|
|
93
|
+
|
|
94
|
+
const renderer = new WebGLRenderer({ antialias: true, alpha: true, powerPreference: "high-performance" });
|
|
95
|
+
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
|
96
|
+
renderer.outputColorSpace = SRGBColorSpace;
|
|
97
|
+
Object.assign(renderer.domElement.style, { display: "block", width: "100%", height: "100%", touchAction: "pan-y" });
|
|
98
|
+
el.prepend(renderer.domElement);
|
|
99
|
+
const scene = new Scene();
|
|
100
|
+
const camera = new PerspectiveCamera(32, 1, 0.1, 200);
|
|
101
|
+
const ring = new Group();
|
|
102
|
+
scene.add(ring);
|
|
103
|
+
|
|
104
|
+
const geometry = new PlaneGeometry(w, h, 32, 1);
|
|
105
|
+
const loader = new TextureLoader().setCrossOrigin("anonymous");
|
|
106
|
+
const fade = new Color(live.current.fadeColor);
|
|
107
|
+
const materials = list.map((item, i) => {
|
|
108
|
+
const m = new ShaderMaterial({
|
|
109
|
+
vertexShader: VERT,
|
|
110
|
+
fragmentShader: FRAG,
|
|
111
|
+
transparent: true,
|
|
112
|
+
side: DoubleSide,
|
|
113
|
+
uniforms: {
|
|
114
|
+
uRadius: { value: R },
|
|
115
|
+
uBase: { value: i * step },
|
|
116
|
+
uTex: { value: null },
|
|
117
|
+
uReady: { value: 0 },
|
|
118
|
+
uCover: { value: new Vector2(1, 1) },
|
|
119
|
+
uCorner: { value: cornerRadius },
|
|
120
|
+
uFade: { value: fade },
|
|
121
|
+
uDepth: { value: depthFade },
|
|
122
|
+
uAspect: { value: aspectRatio },
|
|
123
|
+
uInner: { value: 0.72 },
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
loader.load(item.src, (t) => {
|
|
127
|
+
t.colorSpace = SRGBColorSpace;
|
|
128
|
+
m.uniforms.uTex.value = t;
|
|
129
|
+
const ia = t.image.width / t.image.height;
|
|
130
|
+
// Cover crop: show the middle of the image at the card's shape.
|
|
131
|
+
m.uniforms.uCover.value.set(ia > aspectRatio ? aspectRatio / ia : 1, ia > aspectRatio ? 1 : ia / aspectRatio);
|
|
132
|
+
m.userData.loadedAt = performance.now();
|
|
133
|
+
wake();
|
|
134
|
+
});
|
|
135
|
+
const mesh = new Mesh(geometry, m);
|
|
136
|
+
mesh.renderOrder = 0;
|
|
137
|
+
ring.add(mesh);
|
|
138
|
+
return m;
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const fit = () => {
|
|
142
|
+
const { width, height: hh } = el.getBoundingClientRect();
|
|
143
|
+
if (!width || !hh) return;
|
|
144
|
+
renderer.setSize(width, hh, false);
|
|
145
|
+
camera.aspect = width / hh;
|
|
146
|
+
const vfov = (camera.fov * Math.PI) / 180;
|
|
147
|
+
const hfov = 2 * Math.atan(Math.tan(vfov / 2) * camera.aspect);
|
|
148
|
+
// Distance so the whole ring fits the width and the cards fit the height.
|
|
149
|
+
const dW = (R * 0.98) / Math.tan(hfov / 2);
|
|
150
|
+
const dH = (h * 0.75) / Math.tan(vfov / 2);
|
|
151
|
+
// On narrow screens let the sides crop rather than shrink the front card below half the width.
|
|
152
|
+
const dCard = R + w / (2 * 0.5 * Math.tan(hfov / 2));
|
|
153
|
+
camera.userData.dist = Math.max(Math.min(R + dW, dCard), R + dH);
|
|
154
|
+
camera.updateProjectionMatrix();
|
|
155
|
+
wake();
|
|
156
|
+
};
|
|
157
|
+
const ro = new ResizeObserver(fit);
|
|
158
|
+
ro.observe(el);
|
|
159
|
+
|
|
160
|
+
const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
161
|
+
const s = { angle: 0, vel: 0, target: null, hover: false, drag: null, px: 0, py: 0, yaw: 0, pitch: 0, dir: -1, hold: 0 };
|
|
162
|
+
let raf = 0;
|
|
163
|
+
let last = 0;
|
|
164
|
+
let visible = true;
|
|
165
|
+
let shown = -1;
|
|
166
|
+
|
|
167
|
+
const nearest = (a) => Math.round(a / step) * step;
|
|
168
|
+
const frontIndex = () => ((Math.round(-s.angle / step) % n) + n) % n;
|
|
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 paused = (o.pauseOnHover && s.hover) || s.drag || now < s.hold || reduced;
|
|
176
|
+
const cruise = o.autoplay && !paused ? ((o.speed * Math.PI) / 180) * s.dir : 0;
|
|
177
|
+
let busy = !!s.drag || cruise !== 0;
|
|
178
|
+
if (!s.drag) {
|
|
179
|
+
if (s.target !== null) {
|
|
180
|
+
// Critically damped spring onto the target card.
|
|
181
|
+
const k = 90;
|
|
182
|
+
const c = 2 * Math.sqrt(k);
|
|
183
|
+
s.vel += (k * (s.target - s.angle) - c * s.vel) * dt;
|
|
184
|
+
s.angle += s.vel * dt;
|
|
185
|
+
if (Math.abs(s.target - s.angle) < 0.0005 && Math.abs(s.vel) < 0.001) {
|
|
186
|
+
s.angle = s.target;
|
|
187
|
+
s.vel = 0;
|
|
188
|
+
s.target = null;
|
|
189
|
+
}
|
|
190
|
+
busy = true;
|
|
191
|
+
} else {
|
|
192
|
+
s.vel += (cruise - s.vel) * (1 - Math.exp(-dt / 0.6));
|
|
193
|
+
s.angle += s.vel * dt;
|
|
194
|
+
if (!cruise && o.snap && Math.abs(s.vel) < 0.25) s.target = nearest(s.angle);
|
|
195
|
+
if (Math.abs(s.vel) > 0.0005 || s.target !== null) busy = true;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
s.yaw += (s.px * 0.08 - s.yaw) * (1 - Math.exp(-dt / 0.3));
|
|
199
|
+
s.pitch += (s.py * 0.05 - s.pitch) * (1 - Math.exp(-dt / 0.3));
|
|
200
|
+
if (Math.abs(s.px * 0.08 - s.yaw) > 0.0005) busy = true;
|
|
201
|
+
|
|
202
|
+
ring.rotation.y = s.angle;
|
|
203
|
+
const d = camera.userData.dist || R * 3;
|
|
204
|
+
const t = ((o.tilt * Math.PI) / 180) + s.pitch;
|
|
205
|
+
camera.position.set(Math.sin(s.yaw) * d, -Math.sin(t) * d, Math.cos(s.yaw) * Math.cos(t) * d);
|
|
206
|
+
camera.lookAt(0, 0, 0);
|
|
207
|
+
fade.set(o.fadeColor);
|
|
208
|
+
materials.forEach((m) => {
|
|
209
|
+
m.uniforms.uDepth.value = o.depthFade;
|
|
210
|
+
const at = m.userData.loadedAt;
|
|
211
|
+
if (at) {
|
|
212
|
+
m.uniforms.uReady.value = Math.min(1, (now - at) / 400);
|
|
213
|
+
if (m.uniforms.uReady.value < 1) busy = true;
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
renderer.render(scene, camera);
|
|
217
|
+
|
|
218
|
+
const idx = frontIndex();
|
|
219
|
+
if (idx !== shown) {
|
|
220
|
+
shown = idx;
|
|
221
|
+
setActive(idx);
|
|
222
|
+
o.onChange?.(idx);
|
|
223
|
+
}
|
|
224
|
+
if (busy && visible) raf = requestAnimationFrame(frame);
|
|
225
|
+
else last = 0;
|
|
226
|
+
};
|
|
227
|
+
function wake() {
|
|
228
|
+
if (!raf && visible) raf = requestAnimationFrame(frame);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const goTo = (i) => {
|
|
232
|
+
let target = -i * step;
|
|
233
|
+
target += Math.PI * 2 * Math.round((s.angle - target) / (Math.PI * 2));
|
|
234
|
+
s.target = target;
|
|
235
|
+
s.hold = performance.now() + 2500;
|
|
236
|
+
wake();
|
|
237
|
+
};
|
|
238
|
+
api.current = {
|
|
239
|
+
next: () => goTo(frontIndex() + 1),
|
|
240
|
+
prev: () => goTo(frontIndex() - 1),
|
|
241
|
+
goTo,
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const canvas = renderer.domElement;
|
|
245
|
+
const onDown = (e) => {
|
|
246
|
+
if (e.button !== 0) return;
|
|
247
|
+
s.drag = { x: e.clientX, y: e.clientY, a: s.angle, moved: false, samples: [{ t: performance.now(), a: s.angle }] };
|
|
248
|
+
s.target = null;
|
|
249
|
+
wake();
|
|
250
|
+
};
|
|
251
|
+
const onMove = (e) => {
|
|
252
|
+
const r = canvas.getBoundingClientRect();
|
|
253
|
+
s.px = ((e.clientX - r.left) / r.width) * 2 - 1;
|
|
254
|
+
s.py = ((e.clientY - r.top) / r.height) * 2 - 1;
|
|
255
|
+
const g = s.drag;
|
|
256
|
+
if (g) {
|
|
257
|
+
const dx = e.clientX - g.x;
|
|
258
|
+
if (!g.moved && Math.abs(dx) > 5) {
|
|
259
|
+
g.moved = true;
|
|
260
|
+
canvas.setPointerCapture(e.pointerId);
|
|
261
|
+
}
|
|
262
|
+
if (g.moved) {
|
|
263
|
+
// Drag speed follows the ring's on-screen size.
|
|
264
|
+
s.angle = g.a + (dx / r.width) * 2.4;
|
|
265
|
+
const now = performance.now();
|
|
266
|
+
g.samples.push({ t: now, a: s.angle });
|
|
267
|
+
while (g.samples.length > 2 && now - g.samples[0].t > 100) g.samples.shift();
|
|
268
|
+
s.vel = 0;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
wake();
|
|
272
|
+
};
|
|
273
|
+
const onUp = (e) => {
|
|
274
|
+
const g = s.drag;
|
|
275
|
+
if (!g) return;
|
|
276
|
+
s.drag = null;
|
|
277
|
+
if (g.moved) {
|
|
278
|
+
const a = g.samples[0];
|
|
279
|
+
const b = g.samples[g.samples.length - 1];
|
|
280
|
+
const span = (b.t - a.t) / 1000;
|
|
281
|
+
s.vel = span > 0.01 ? Math.max(-12, Math.min(12, (b.a - a.a) / span)) : 0;
|
|
282
|
+
if (Math.abs(s.vel) > 0.6) s.dir = Math.sign(s.vel);
|
|
283
|
+
s.hold = performance.now() + 1200;
|
|
284
|
+
} else {
|
|
285
|
+
// A click: the front-facing card whose center lands nearest the pointer.
|
|
286
|
+
const r = canvas.getBoundingClientRect();
|
|
287
|
+
const x = ((e.clientX - r.left) / r.width - 0.5) * 2;
|
|
288
|
+
const p = new Vector3();
|
|
289
|
+
let best = -1;
|
|
290
|
+
let bestDx = Infinity;
|
|
291
|
+
for (let i = 0; i < n; i++) {
|
|
292
|
+
const a = i * step + s.angle;
|
|
293
|
+
if (Math.cos(a) < 0.15) continue;
|
|
294
|
+
const dx = Math.abs(p.set(Math.sin(a) * R, 0, Math.cos(a) * R).project(camera).x - x);
|
|
295
|
+
if (dx < bestDx) {
|
|
296
|
+
bestDx = dx;
|
|
297
|
+
best = i;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (best >= 0) goTo(best);
|
|
301
|
+
}
|
|
302
|
+
wake();
|
|
303
|
+
};
|
|
304
|
+
const onEnter = () => {
|
|
305
|
+
s.hover = true;
|
|
306
|
+
wake();
|
|
307
|
+
};
|
|
308
|
+
const onLeave = () => {
|
|
309
|
+
s.hover = false;
|
|
310
|
+
s.px = 0;
|
|
311
|
+
s.py = 0;
|
|
312
|
+
if (s.drag) onUp({ clientX: 0, clientY: 0 });
|
|
313
|
+
wake();
|
|
314
|
+
};
|
|
315
|
+
canvas.addEventListener("pointerdown", onDown);
|
|
316
|
+
canvas.addEventListener("pointermove", onMove);
|
|
317
|
+
canvas.addEventListener("pointerup", onUp);
|
|
318
|
+
canvas.addEventListener("pointercancel", onUp);
|
|
319
|
+
canvas.addEventListener("pointerenter", onEnter);
|
|
320
|
+
canvas.addEventListener("pointerleave", onLeave);
|
|
321
|
+
const io = new IntersectionObserver(([entry]) => {
|
|
322
|
+
visible = entry.isIntersecting;
|
|
323
|
+
wake();
|
|
324
|
+
});
|
|
325
|
+
io.observe(el);
|
|
326
|
+
fit();
|
|
327
|
+
|
|
328
|
+
return () => {
|
|
329
|
+
cancelAnimationFrame(raf);
|
|
330
|
+
ro.disconnect();
|
|
331
|
+
io.disconnect();
|
|
332
|
+
canvas.removeEventListener("pointerdown", onDown);
|
|
333
|
+
canvas.removeEventListener("pointermove", onMove);
|
|
334
|
+
canvas.removeEventListener("pointerup", onUp);
|
|
335
|
+
canvas.removeEventListener("pointercancel", onUp);
|
|
336
|
+
canvas.removeEventListener("pointerenter", onEnter);
|
|
337
|
+
canvas.removeEventListener("pointerleave", onLeave);
|
|
338
|
+
materials.forEach((m) => {
|
|
339
|
+
m.uniforms.uTex.value?.dispose();
|
|
340
|
+
m.dispose();
|
|
341
|
+
});
|
|
342
|
+
geometry.dispose();
|
|
343
|
+
renderer.dispose();
|
|
344
|
+
renderer.forceContextLoss();
|
|
345
|
+
canvas.remove();
|
|
346
|
+
api.current = null;
|
|
347
|
+
};
|
|
348
|
+
// Rebuild when the cards or their shape change; motion settings are live.
|
|
349
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
350
|
+
}, [key, cardWidth, aspectRatio, gap, cornerRadius]);
|
|
351
|
+
|
|
352
|
+
const current = list[active] ?? list[0];
|
|
353
|
+
return (
|
|
354
|
+
<div
|
|
355
|
+
ref={wrap}
|
|
356
|
+
role="region"
|
|
357
|
+
aria-roledescription="carousel"
|
|
358
|
+
aria-label="Image carousel"
|
|
359
|
+
tabIndex={0}
|
|
360
|
+
onKeyDown={(e) => {
|
|
361
|
+
if (e.key === "ArrowRight") api.current?.next();
|
|
362
|
+
else if (e.key === "ArrowLeft") api.current?.prev();
|
|
363
|
+
else return;
|
|
364
|
+
e.preventDefault();
|
|
365
|
+
}}
|
|
366
|
+
className={`relative w-full cursor-grab select-none outline-none active:cursor-grabbing focus-visible:ring-2 focus-visible:ring-white/30 ${className}`}
|
|
367
|
+
style={{ height }}
|
|
368
|
+
>
|
|
369
|
+
{captions && current && (
|
|
370
|
+
<div className="pointer-events-none absolute inset-x-0 bottom-3 text-center" aria-live="polite">
|
|
371
|
+
<p className="text-sm font-semibold">{current.title}</p>
|
|
372
|
+
<p className="font-mono text-[0.7rem] tabular-nums opacity-50">
|
|
373
|
+
{String(active + 1).padStart(2, "0")} / {String(list.length).padStart(2, "0")}
|
|
374
|
+
</p>
|
|
375
|
+
</div>
|
|
376
|
+
)}
|
|
377
|
+
</div>
|
|
378
|
+
);
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
export default CircularCarousel;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Edge Blur: a soft, progressive blur along one edge of a scrolling area,
|
|
5
|
+
* the way content melts under an app's top bar or fades out at the end of a
|
|
6
|
+
* list. It stacks a few backdrop-blur layers; each one doubles the blur of
|
|
7
|
+
* the last and is masked to an overlapping band, so the blur grows smoothly
|
|
8
|
+
* toward the edge instead of in steps. Optionally fades into a tint, and can
|
|
9
|
+
* hide itself when the scroll reaches that edge.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const DIR = { top: "to top", bottom: "to bottom", left: "to left", right: "to right" };
|
|
13
|
+
const len = (v) => (typeof v === "number" ? `${v}px` : v);
|
|
14
|
+
|
|
15
|
+
// The nearest thing that scrolls along this axis: a sibling, or the parent.
|
|
16
|
+
function findScroller(el, vertical) {
|
|
17
|
+
const scrolls = (node) => {
|
|
18
|
+
const s = getComputedStyle(node);
|
|
19
|
+
return /(auto|scroll)/.test(vertical ? s.overflowY : s.overflowX);
|
|
20
|
+
};
|
|
21
|
+
const parent = el.parentElement;
|
|
22
|
+
if (!parent) return null;
|
|
23
|
+
return [...parent.children].find((c) => c !== el && scrolls(c)) || (scrolls(parent) ? parent : null);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const EdgeBlur = ({ edge = "bottom", size = "6rem", blur = 16, steps = 6, tint = "transparent", hideAtEnd = false, scroller, fixed = false, zIndex = 10, className = "" }) => {
|
|
27
|
+
const ref = useRef(null);
|
|
28
|
+
const side = DIR[edge] ? edge : "bottom";
|
|
29
|
+
const vertical = side === "top" || side === "bottom";
|
|
30
|
+
const count = Math.max(2, Math.min(10, Math.round(steps)));
|
|
31
|
+
|
|
32
|
+
// Layer i covers the band [i, i + 2] / count and blurs blur / 2^(count - 1 - i).
|
|
33
|
+
const layers = useMemo(
|
|
34
|
+
() =>
|
|
35
|
+
Array.from({ length: count }, (_, i) => {
|
|
36
|
+
const a = (i / count) * 100;
|
|
37
|
+
const b = ((i + 1) / count) * 100;
|
|
38
|
+
const c = ((i + 2) / count) * 100;
|
|
39
|
+
const amount = blur / Math.pow(2, count - 1 - i);
|
|
40
|
+
const last = i === count - 1;
|
|
41
|
+
const mask = `linear-gradient(${DIR[side]}, transparent ${a.toFixed(1)}%, #000 ${b.toFixed(1)}%${last ? "" : `, #000 ${Math.min(100, c).toFixed(1)}%, transparent ${Math.min(100, c + 100 / count).toFixed(1)}%`})`;
|
|
42
|
+
return { filter: `blur(${amount.toFixed(2)}px)`, mask };
|
|
43
|
+
}),
|
|
44
|
+
[count, blur, side],
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
const el = ref.current;
|
|
49
|
+
if (!hideAtEnd || !el) {
|
|
50
|
+
if (el) el.style.opacity = "";
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
const target = scroller?.current ?? scroller ?? findScroller(el, vertical);
|
|
54
|
+
if (!target) return undefined;
|
|
55
|
+
const start = side === "top" || side === "left";
|
|
56
|
+
const update = () => {
|
|
57
|
+
const pos = vertical ? target.scrollTop : target.scrollLeft;
|
|
58
|
+
const max = vertical ? target.scrollHeight - target.clientHeight : target.scrollWidth - target.clientWidth;
|
|
59
|
+
const room = start ? pos : max - pos;
|
|
60
|
+
const reach = Math.max(1, vertical ? el.offsetHeight : el.offsetWidth);
|
|
61
|
+
el.style.opacity = String(Math.max(0, Math.min(1, room / reach)));
|
|
62
|
+
};
|
|
63
|
+
update();
|
|
64
|
+
target.addEventListener("scroll", update, { passive: true });
|
|
65
|
+
const ro = new ResizeObserver(update);
|
|
66
|
+
ro.observe(target);
|
|
67
|
+
return () => {
|
|
68
|
+
target.removeEventListener("scroll", update);
|
|
69
|
+
ro.disconnect();
|
|
70
|
+
};
|
|
71
|
+
}, [hideAtEnd, scroller, side, vertical]);
|
|
72
|
+
|
|
73
|
+
const box = {
|
|
74
|
+
position: fixed ? "fixed" : "absolute",
|
|
75
|
+
zIndex,
|
|
76
|
+
pointerEvents: "none",
|
|
77
|
+
transition: "opacity 0.25s ease-out",
|
|
78
|
+
[side]: 0,
|
|
79
|
+
...(vertical ? { left: 0, right: 0, height: len(size) } : { top: 0, bottom: 0, width: len(size) }),
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<div ref={ref} aria-hidden="true" className={className} style={box}>
|
|
84
|
+
{layers.map((l, i) => (
|
|
85
|
+
<div key={i} className="absolute inset-0" style={{ backdropFilter: l.filter, WebkitBackdropFilter: l.filter, maskImage: l.mask, WebkitMaskImage: l.mask }} />
|
|
86
|
+
))}
|
|
87
|
+
{tint !== "transparent" && <div className="absolute inset-0" style={{ background: `linear-gradient(${DIR[side]}, transparent, ${tint})` }} />}
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export default EdgeBlur;
|
|
@@ -91,7 +91,10 @@ const FlipClock = ({ mode = "seconds", target, seconds = 3 * 86400 + 4 * 3600 +
|
|
|
91
91
|
const ink = dark ? "#fafafa" : "#18181b";
|
|
92
92
|
|
|
93
93
|
return (
|
|
94
|
-
|
|
94
|
+
// The clock is sized in em; on narrow screens the font shrinks with the
|
|
95
|
+
// container (cqw) so every unit fits instead of being cut off.
|
|
96
|
+
<div className="flex w-full justify-center" style={{ containerType: "inline-size" }}>
|
|
97
|
+
<div role="timer" aria-live="off" aria-label={parts.map(([l, v]) => `${v} ${l}`).join(", ")} className={`inline-flex items-start gap-[0.35em] font-mono font-bold ${className}`} style={{ fontSize: `min(${size}rem, ${(100 / (parts.length * 2.55)).toFixed(2)}cqw)` }}>
|
|
95
98
|
{parts.map(([label, value], i) => (
|
|
96
99
|
<React.Fragment key={label}>
|
|
97
100
|
{i > 0 && (
|
|
@@ -115,6 +118,7 @@ const FlipClock = ({ mode = "seconds", target, seconds = 3 * 86400 + 4 * 3600 +
|
|
|
115
118
|
</React.Fragment>
|
|
116
119
|
))}
|
|
117
120
|
</div>
|
|
121
|
+
</div>
|
|
118
122
|
);
|
|
119
123
|
};
|
|
120
124
|
|
|
@@ -289,7 +289,7 @@ const GooeyNav = ({ items = DEFAULT_ITEMS, initialActive = 0, onChange, accentCo
|
|
|
289
289
|
|
|
290
290
|
return (
|
|
291
291
|
<nav className={`relative inline-flex max-w-full ${className}`} aria-label="Main">
|
|
292
|
-
<div ref={bar} className={`relative flex items-center gap-0
|
|
292
|
+
<div ref={bar} className={`relative flex items-center gap-0 rounded-full p-1 sm:gap-0.5 sm:p-1.5 ${dark ? "bg-neutral-950 ring-1 ring-white/10" : "bg-white shadow-sm ring-1 ring-black/10"}`}>
|
|
293
293
|
<canvas ref={canvas} aria-hidden="true" className="pointer-events-none absolute" style={{ left: -PAD, top: -PAD, display: fallback ? "none" : "block" }} />
|
|
294
294
|
{fallback && <span ref={fallbackPill} aria-hidden="true" className="pointer-events-none absolute rounded-full" style={{ background: accentColor }} />}
|
|
295
295
|
{items.map((item, i) => (
|
|
@@ -299,7 +299,7 @@ const GooeyNav = ({ items = DEFAULT_ITEMS, initialActive = 0, onChange, accentCo
|
|
|
299
299
|
href={item.href}
|
|
300
300
|
onClick={(e) => select(i, e)}
|
|
301
301
|
aria-current={i === active ? "page" : undefined}
|
|
302
|
-
className={`relative z-[1] shrink-0 rounded-full px-
|
|
302
|
+
className={`relative z-[1] shrink-0 rounded-full px-2.5 py-2 text-[0.82rem] font-medium sm:py-2.5 sm:text-[0.95rem] outline-none transition-colors duration-300 focus-visible:ring-2 focus-visible:ring-current/30 sm:px-5 ${i === active ? "text-neutral-950" : dark ? "text-white/65 hover:text-white" : "text-neutral-500 hover:text-neutral-900"}`}
|
|
303
303
|
>
|
|
304
304
|
{item.label}
|
|
305
305
|
</a>
|