header close fix and visual fixes

This commit is contained in:
2026-04-24 13:59:13 +07:00
parent 811bfb99b7
commit 4aac2501cb
3 changed files with 41 additions and 32 deletions
+29 -22
View File
@@ -15,9 +15,6 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
const workerRef = useRef<Worker | null>(null); const workerRef = useRef<Worker | null>(null);
const dimensionsRef = useRef({ width: 0, height: 0 }); const dimensionsRef = useRef({ width: 0, height: 0 });
const [isLoaded, setIsLoaded] = useState(false); const [isLoaded, setIsLoaded] = useState(false);
// Используем Ref для отслеживания того, был ли уже передан контроль.
// Это критично, так как React в StrictMode или при смене языка может дергать эффект дважды.
const hasTransferred = useRef(false); const hasTransferred = useRef(false);
const { resolvedTheme, theme } = useTheme(); const { resolvedTheme, theme } = useTheme();
@@ -37,6 +34,7 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
return window.innerWidth <= 768 || "ontouchstart" in window; return window.innerWidth <= 768 || "ontouchstart" in window;
}; };
// 1. ИНИЦИАЛИЗАЦИЯ ВОРКЕРА
useEffect(() => { useEffect(() => {
if ( if (
!mounted || !mounted ||
@@ -48,7 +46,6 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
const canvas = canvasRef.current; const canvas = canvasRef.current;
const container = containerRef.current; const container = containerRef.current;
// ГЛАВНЫЙ ФИКС: Если контроль уже передан, ничего не делаем с этим canvas
if (!canvas || !container || hasTransferred.current) return; if (!canvas || !container || hasTransferred.current) return;
const isMobile = checkIsMobile(); const isMobile = checkIsMobile();
@@ -61,7 +58,6 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
dimensionsRef.current = { width: initialWidth, height: initialHeight }; dimensionsRef.current = { width: initialWidth, height: initialHeight };
try { try {
// 1. Устанавливаем размер ТОЛЬКО ОДИН РАЗ до вызова transfer
canvas.width = initialWidth; canvas.width = initialWidth;
canvas.height = initialHeight; canvas.height = initialHeight;
@@ -70,9 +66,8 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
}); });
workerRef.current = worker; workerRef.current = worker;
// 2. Передаем контроль
const offscreen = canvas.transferControlToOffscreen(); const offscreen = canvas.transferControlToOffscreen();
hasTransferred.current = true; // Блокируем любые изменения canvas.width навсегда для этого инстанса hasTransferred.current = true;
worker.postMessage( worker.postMessage(
{ {
@@ -96,12 +91,9 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
if (!container || !workerRef.current) return; if (!container || !workerRef.current) return;
const isMob = checkIsMobile(); const isMob = checkIsMobile();
const scale = isMob ? 1.0 : Math.min(window.devicePixelRatio || 1, 1.5); const scale = isMob ? 1.0 : Math.min(window.devicePixelRatio || 1, 1.5);
const w = container.clientWidth * scale; const w = container.clientWidth * scale;
const h = container.clientHeight * scale; const h = container.clientHeight * scale;
dimensionsRef.current = { width: w, height: h };
dimensionsRef.current = { width: w, height: h }; // Обновляем тут
workerRef.current.postMessage({ workerRef.current.postMessage({
type: "RESIZE", type: "RESIZE",
payload: { payload: {
@@ -121,15 +113,14 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
workerRef.current.terminate(); workerRef.current.terminate();
workerRef.current = null; workerRef.current = null;
} }
// Сбрасываем флаг, так как DOM-элемент canvas будет удален и заменен новым
hasTransferred.current = false; hasTransferred.current = false;
}; };
} catch (e) { } catch (e) {
console.error("Matrix Worker Bridge Failed:", e); console.error("Matrix Worker Bridge Failed:", e);
} }
}, [mounted]); // Зависим только от mounted }, [mounted]);
// Синхронизация темы // 2. ОБРАБОТКА ТЕМЫ
useEffect(() => { useEffect(() => {
if (workerRef.current && mounted) { if (workerRef.current && mounted) {
workerRef.current.postMessage({ workerRef.current.postMessage({
@@ -139,16 +130,27 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
} }
}, [isSecure, isDarkMode, mounted]); }, [isSecure, isDarkMode, mounted]);
// Обработка событий (clientX и т.д.) // 3. ОБРАБОТКА КЛИКОВ И ГЛИТЧЕЙ (handleEvents теперь внутри эффекта)
useEffect(() => { useEffect(() => {
if (!mounted) return;
const handleEvents = (e: any) => { const handleEvents = (e: any) => {
if (!workerRef.current || !canvasRef.current) return; if (!workerRef.current || !canvasRef.current) return;
// Фильтр интерактивных элементов
const isInteractive = e.target.closest(
'button, a, input, [role="button"], [role="switch"], .data-\\[state\\]',
);
const isDown = e.type === "pointerdown" || e.type === "touchstart"; const isDown = e.type === "pointerdown" || e.type === "touchstart";
const isUp = const isUp =
e.type === "pointerup" || e.type === "pointerup" ||
e.type === "touchend" || e.type === "touchend" ||
e.type === "pointercancel"; e.type === "pointercancel";
// Если нажали на кнопку — не пускаем событие DRAW_START в воркер
if (isInteractive && isDown) return;
const type = isDown ? "DRAW_START" : isUp ? "DRAW_END" : "MOUSE_MOVE"; const type = isDown ? "DRAW_START" : isUp ? "DRAW_END" : "MOUSE_MOVE";
let clientX: number | null = null; let clientX: number | null = null;
@@ -180,26 +182,31 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
}); });
}; };
window.addEventListener("pointermove", handleEvents); const options = { passive: true };
window.addEventListener("pointerdown", handleEvents); window.addEventListener("pointerdown", handleEvents);
window.addEventListener("pointermove", handleEvents);
window.addEventListener("pointerup", handleEvents); window.addEventListener("pointerup", handleEvents);
window.addEventListener("pointercancel", handleEvents);
window.addEventListener("touchstart", handleEvents, options);
window.addEventListener("touchmove", handleEvents, options);
window.addEventListener("touchend", handleEvents, options);
return () => { return () => {
window.removeEventListener("pointermove", handleEvents);
window.removeEventListener("pointerdown", handleEvents); window.removeEventListener("pointerdown", handleEvents);
window.removeEventListener("pointermove", handleEvents);
window.removeEventListener("pointerup", handleEvents); window.removeEventListener("pointerup", handleEvents);
window.removeEventListener("pointercancel", handleEvents);
window.removeEventListener("touchstart", handleEvents);
window.removeEventListener("touchmove", handleEvents);
window.removeEventListener("touchend", handleEvents);
}; };
}, [mounted]); }, [mounted]); // Подписываемся на события после маунта
return ( return (
<div <div
ref={containerRef} ref={containerRef}
className="absolute inset-0 overflow-hidden z-0 bg-background" className="absolute inset-0 overflow-hidden z-0 bg-background"
> >
{/* КЛЮЧЕВОЙ МОМЕНТ: Добавляем key={resolvedTheme + mounted}.
Это заставит React полностью удалять старый DOM-узел <canvas> и создавать новый.
Это единственный надежный способ сбросить состояние OffscreenControl в браузере.
*/}
<canvas <canvas
key="matrix-canvas" key="matrix-canvas"
ref={canvasRef} ref={canvasRef}
+3 -2
View File
@@ -36,7 +36,8 @@ export function Header({
className="flex items-center gap-2 md:gap-3 group hover:scale-105 transition-transform duration-300 w-fit relative z-50" className="flex items-center gap-2 md:gap-3 group hover:scale-105 transition-transform duration-300 w-fit relative z-50"
onClick={() => setIsMenuOpen(false)} onClick={() => setIsMenuOpen(false)}
> >
<div className="text-lg sm:text-2xl md:text-3xl tracking-wide md:tracking-widest flex items-center select-none font-bold uppercase"> {/* Изменено: text-xl (вместо text-lg) для мобилок, sm:text-2xl для маленьких планшетов */}
<div className="text-xl sm:text-2xl md:text-3xl tracking-wide md:tracking-widest flex items-center select-none font-bold uppercase">
<span <span
className="text-transparent animate-neon-flicker transition-all duration-300" className="text-transparent animate-neon-flicker transition-all duration-300"
style={{ style={{
@@ -78,7 +79,6 @@ export function Header({
href={`/${lang}/download`} href={`/${lang}/download`}
variant="primary" variant="primary"
className="hidden sm:block scale-75 md:scale-90 origin-right" className="hidden sm:block scale-75 md:scale-90 origin-right"
onClick={() => setIsMenuOpen(false)}
> >
{dict.cta} {dict.cta}
</CyberButton> </CyberButton>
@@ -170,6 +170,7 @@ export function Header({
href={`/${lang}/download`} href={`/${lang}/download`}
variant="primary" variant="primary"
className="w-full text-sm py-3" className="w-full text-sm py-3"
onClick={() => setIsMenuOpen(false)}
> >
<span className="flex items-center justify-center gap-2"> <span className="flex items-center justify-center gap-2">
{dict.cta} <ArrowRight className="size-4" /> {dict.cta} <ArrowRight className="size-4" />
+9 -8
View File
@@ -236,7 +236,6 @@ function CyberWatcherEyes({ vpnOn }: { vpnOn: boolean }) {
</div> </div>
); );
} }
export function Hero({ export function Hero({
dict, dict,
lang, lang,
@@ -250,22 +249,25 @@ export function Hero({
return ( return (
<section className="relative pt-32 pb-20 md:pt-48 md:pb-32 flex flex-col items-center text-center px-4 min-h-[95vh] justify-center overflow-hidden select-none"> <section className="relative pt-32 pb-20 md:pt-48 md:pb-32 flex flex-col items-center text-center px-4 min-h-[95vh] justify-center overflow-hidden select-none">
{/* --- RUST BACKGROUND --- */} {/* --- RUST BACKGROUND --- */}
{/* Фон всегда активен для кликов */}
<div className="absolute inset-0 -z-10 bg-background pointer-events-auto"> <div className="absolute inset-0 -z-10 bg-background pointer-events-auto">
<NetrunnerMatrix isSecure={isVpnOn} key={lang} /> <NetrunnerMatrix isSecure={isVpnOn} key={lang} />
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,_transparent_20%,_var(--background)_120%)] opacity-80 pointer-events-none" /> <div className="absolute inset-0 bg-[radial-gradient(circle_at_center,_transparent_20%,_var(--background)_120%)] opacity-80 pointer-events-none" />
<div className="absolute inset-x-0 top-0 h-64 bg-gradient-to-b from-background via-transparent to-transparent opacity-90 pointer-events-none" /> <div className="absolute inset-x-0 top-0 h-64 bg-gradient-to-b from-background via-transparent to-transparent opacity-90 pointer-events-none" />
</div> </div>
{/* КОНТЕНТ: Убираем pointer-events-none отсюда, чтобы не блокировать всё окно */}
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }} transition={{ duration: 0.5 }}
className="relative z-10 w-full max-w-4xl mx-auto pt-40 md:pt-48 pb-20 pointer-events-none" className="relative z-10 w-full max-w-4xl mx-auto pt-40 md:pt-48 pb-20 pointer-events-none" // Оставляем none здесь, чтобы клики шли "сквозь" пустые места текста на канвас
> >
<div className="pointer-events-auto flex flex-col items-center"> <div className="flex flex-col items-center">
{/* Каждый элемент управления должен быть pointer-events-auto */}
<motion.div <motion.div
layout layout
className={`inline-flex items-center gap-2 px-4 py-1.5 rounded-full border text-sm font-bold mb-8 transition-colors duration-500 shadow-sm cursor-default ${ className={`inline-flex items-center gap-2 px-4 py-1.5 rounded-full border text-sm font-bold mb-8 transition-colors duration-500 shadow-sm cursor-default pointer-events-auto ${
isVpnOn isVpnOn
? "bg-primary/10 border-primary/50 text-primary" ? "bg-primary/10 border-primary/50 text-primary"
: "bg-secondary/10 border-secondary/50 text-secondary" : "bg-secondary/10 border-secondary/50 text-secondary"
@@ -279,7 +281,6 @@ export function Hero({
<span>{isVpnOn ? dict.badgeSecure : dict.badge}</span> <span>{isVpnOn ? dict.badgeSecure : dict.badge}</span>
</motion.div> </motion.div>
{/* НАШИ ИДЕАЛЬНЫЕ ГЛАЗА */}
<CyberWatcherEyes vpnOn={isVpnOn} /> <CyberWatcherEyes vpnOn={isVpnOn} />
<h1 className="text-4xl md:text-6xl font-extrabold tracking-tight text-foreground mb-6 font-mono cursor-default"> <h1 className="text-4xl md:text-6xl font-extrabold tracking-tight text-foreground mb-6 font-mono cursor-default">
@@ -299,8 +300,8 @@ export function Hero({
{dict.subtitle} {dict.subtitle}
</p> </p>
<div className="flex flex-col items-center gap-8 mb-12"> <div className="flex flex-col items-center gap-8 mb-12 pointer-events-none">
<div className="flex items-center gap-4 bg-card/80 backdrop-blur-xl border border-border p-4 rounded-3xl shadow-md transition-colors duration-300"> <div className="flex items-center gap-4 bg-card/80 backdrop-blur-xl border border-border p-4 rounded-3xl shadow-md transition-colors duration-300 pointer-events-auto">
<Switch <Switch
checked={isVpnOn} checked={isVpnOn}
onCheckedChange={setIsVpnOn} onCheckedChange={setIsVpnOn}
@@ -315,7 +316,7 @@ export function Hero({
</span> </span>
</div> </div>
<div className="flex flex-col sm:flex-row gap-6 justify-center w-full sm:w-auto font-mono"> <div className="flex flex-col sm:flex-row gap-6 justify-center w-full sm:w-auto font-mono pointer-events-auto">
<CyberButton <CyberButton
href={`/${lang}/download`} href={`/${lang}/download`}
variant={isVpnOn ? "primary" : "secondary"} variant={isVpnOn ? "primary" : "secondary"}