header close fix and visual fixes
This commit is contained in:
@@ -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}
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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"}
|
||||||
|
|||||||
Reference in New Issue
Block a user