mirror of
https://github.com/vercel/workflow.git
synced 2026-09-14 19:59:43 +08:00
32ac8e73fd
* Fix Biome lint violations and add Biome CI check Biome was not configured to respect .gitignore, so ~92% of the 13,355 reported diagnostics came from gitignored build artifacts. Enable VCS integration (useIgnoreFile), apply safe auto-fixes across the repo, fix the remaining mechanical errors by hand, downgrade judgment-call a11y / dangerouslySetInnerHTML rules to warnings, and add a 'biome ci' job to the Lint workflow so violations block PRs going forward. * Use an empty changeset (no behavior change, no release needed)
127 lines
3.5 KiB
TypeScript
127 lines
3.5 KiB
TypeScript
'use client';
|
|
|
|
import createGlobe, { type COBEOptions } from 'cobe';
|
|
import { useTheme } from 'next-themes';
|
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
|
|
interface GlobeProps {
|
|
className?: string;
|
|
}
|
|
|
|
export function Globe({ className }: GlobeProps) {
|
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const pointerInteracting = useRef<number | null>(null);
|
|
const pointerInteractionMovement = useRef(0);
|
|
const [size, setSize] = useState(1200);
|
|
const [mounted, setMounted] = useState(false);
|
|
const { resolvedTheme } = useTheme();
|
|
|
|
// Defer theme-dependent rendering until mounted to avoid hydration mismatch
|
|
useEffect(() => {
|
|
setMounted(true);
|
|
}, []);
|
|
|
|
const updatePointerInteraction = useCallback((value: number | null) => {
|
|
pointerInteracting.current = value;
|
|
if (canvasRef.current) {
|
|
canvasRef.current.style.cursor = value !== null ? 'grabbing' : 'grab';
|
|
}
|
|
}, []);
|
|
|
|
const updateMovement = useCallback((clientX: number) => {
|
|
if (pointerInteracting.current !== null) {
|
|
const delta = clientX - pointerInteracting.current;
|
|
pointerInteractionMovement.current = delta;
|
|
pointerInteracting.current = clientX;
|
|
}
|
|
}, []);
|
|
|
|
// Track container width
|
|
useEffect(() => {
|
|
const updateSize = () => {
|
|
if (containerRef.current) {
|
|
const width = containerRef.current.offsetWidth;
|
|
setSize(width);
|
|
}
|
|
};
|
|
|
|
updateSize();
|
|
window.addEventListener('resize', updateSize);
|
|
return () => window.removeEventListener('resize', updateSize);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!canvasRef.current || size === 0 || !mounted) return;
|
|
|
|
let phi = 0;
|
|
const canvasSize = size * 2; // For retina displays
|
|
const isDark = resolvedTheme === 'dark';
|
|
|
|
const globeConfig: COBEOptions = {
|
|
devicePixelRatio: 2,
|
|
width: canvasSize,
|
|
height: canvasSize,
|
|
phi: 0,
|
|
theta: 0.3,
|
|
dark: isDark ? 1 : 0,
|
|
diffuse: isDark ? 2 : 1.2,
|
|
mapSamples: 16000,
|
|
mapBrightness: isDark ? 4 : 2,
|
|
baseColor: isDark ? [0.3, 0.3, 0.3] : [1, 1, 1],
|
|
markerColor: [0.251, 0.678, 1],
|
|
glowColor: isDark ? [0.15, 0.15, 0.15] : [0.95, 0.95, 0.95],
|
|
markers: [],
|
|
onRender: (state) => {
|
|
if (pointerInteracting.current === null) {
|
|
phi += 0.003;
|
|
}
|
|
phi += pointerInteractionMovement.current * 0.01;
|
|
pointerInteractionMovement.current *= 0.95;
|
|
|
|
state.phi = phi;
|
|
state.width = canvasSize;
|
|
state.height = canvasSize;
|
|
},
|
|
};
|
|
|
|
const globe = createGlobe(canvasRef.current, globeConfig);
|
|
|
|
setTimeout(() => {
|
|
if (canvasRef.current) {
|
|
canvasRef.current.style.opacity = '1';
|
|
}
|
|
}, 100);
|
|
|
|
return () => {
|
|
globe.destroy();
|
|
};
|
|
}, [size, resolvedTheme, mounted]);
|
|
|
|
return (
|
|
<div ref={containerRef} className={className} style={{ aspectRatio: '1' }}>
|
|
<canvas
|
|
ref={canvasRef}
|
|
className="w-full h-full opacity-0 transition-opacity duration-500 cursor-grab"
|
|
onPointerDown={(e) => {
|
|
updatePointerInteraction(e.clientX);
|
|
}}
|
|
onPointerUp={() => {
|
|
updatePointerInteraction(null);
|
|
}}
|
|
onPointerOut={() => {
|
|
updatePointerInteraction(null);
|
|
}}
|
|
onMouseMove={(e) => {
|
|
updateMovement(e.clientX);
|
|
}}
|
|
onTouchMove={(e) => {
|
|
if (e.touches[0]) {
|
|
updateMovement(e.touches[0].clientX);
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|