feat: unify code-like components with shared CodeWindow, fix blog re-render loop, and stabilize layouts
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🧪 QA (push) Failing after 1m2s
Build & Deploy / 🏗️ Build (push) Failing after 3m44s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🧪 QA (push) Failing after 1m2s
Build & Deploy / 🏗️ Build (push) Failing after 3m44s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
This commit is contained in:
@@ -3,8 +3,9 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
import * as React from "react";
|
||||
import { cn } from "../utils/cn";
|
||||
import { ShieldCheck } from "lucide-react";
|
||||
import { MonoLabel } from "./Typography";
|
||||
import { ShieldCheck, ArrowLeft, ArrowRight, RefreshCw } from "lucide-react";
|
||||
import { MonoLabel, Label } from "./Typography";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
|
||||
interface IframeSectionProps {
|
||||
src: string;
|
||||
@@ -24,42 +25,90 @@ interface IframeSectionProps {
|
||||
delay?: number;
|
||||
noScale?: boolean;
|
||||
dynamicGlow?: boolean;
|
||||
minHeight?: number;
|
||||
mobileWidth?: number;
|
||||
}
|
||||
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
/**
|
||||
* Reusable Browser UI components to maintain consistency
|
||||
*/
|
||||
const BrowserChrome: React.FC<{ url: string; minimal?: boolean }> = ({
|
||||
const BrowserChromeComponent: React.FC<{ url: string; minimal?: boolean }> = ({
|
||||
url,
|
||||
minimal,
|
||||
}) => {
|
||||
if (minimal) return null;
|
||||
return (
|
||||
<div className="h-14 bg-white/90 backdrop-blur-2xl border-b border-slate-200/40 flex items-center px-6 gap-8 z-[100] flex-shrink-0 relative">
|
||||
{/* Status Indicators (Traffic Lights) */}
|
||||
<div className="flex gap-1.5 opacity-40">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-slate-900" />
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-slate-900" />
|
||||
<div
|
||||
className="h-12 md:h-14 bg-slate-50 border-b border-slate-200/60 flex items-center px-4 md:px-6 gap-4 md:gap-8 z-[100] flex-shrink-0 relative isolation-auto"
|
||||
style={{ isolation: "isolate", minHeight: "48px" }}
|
||||
>
|
||||
{/* 3D Rim Highlight */}
|
||||
<div className="absolute top-0 left-0 right-0 h-[1px] bg-white opacity-80" />
|
||||
|
||||
{/* Status Indicators (Traffic Lights) - Enhanced with subtle depth */}
|
||||
<div className="flex gap-1.5 md:gap-2.5">
|
||||
{[
|
||||
"bg-slate-300 from-slate-200 to-slate-400",
|
||||
"bg-slate-300 from-slate-200 to-slate-400",
|
||||
"bg-slate-300 from-slate-200 to-slate-400",
|
||||
].map((cls, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={cn(
|
||||
"w-2.5 h-2.5 md:w-3 md:h-3 rounded-full bg-gradient-to-br shadow-[inset_0_1px_2px_rgba(0,0,0,0.1),0_1.5px_1px_rgba(255,255,255,0.7)] relative flex items-center justify-center",
|
||||
cls,
|
||||
)}
|
||||
>
|
||||
{/* Subtle glint, soft blur */}
|
||||
<div className="absolute top-[20%] left-[20%] w-[30%] h-[30%] rounded-full bg-white/30 blur-[0.7px]" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* URL Bar */}
|
||||
<div className="flex-1 max-w-[600px] mx-auto bg-white/30 backdrop-blur-3xl rounded-full flex items-center justify-center px-6 h-8 border border-white/60 shadow-[0_2px_12px_-4px_rgba(0,0,0,0.08)]">
|
||||
<div className="flex items-center gap-3 opacity-80 group-hover:opacity-100 transition-all duration-700">
|
||||
<ShieldCheck className="w-3.5 h-3.5 text-slate-900" />
|
||||
<span className="text-[10px] font-mono font-bold tracking-[0.25em] uppercase truncate whitespace-nowrap text-slate-900">
|
||||
{url}
|
||||
{/* Navigation Controls - Hidden on mobile */}
|
||||
<div className="hidden lg:flex items-center gap-4 opacity-30">
|
||||
<ArrowLeft className="w-3.5 h-3.5" />
|
||||
<ArrowRight className="w-3.5 h-3.5" />
|
||||
<RefreshCw className="w-3 h-3 ml-1" />
|
||||
</div>
|
||||
|
||||
{/* URL Bar - Solid high-fidelity layer instead of expensive backdrop-blur */}
|
||||
<div className="flex-1 max-w-[700px] mx-auto bg-white shadow-[inset_0_1px_3px_rgba(0,0,0,0.06),0_0_0_1px_rgba(255,255,255,0.4)] rounded-xl flex items-center justify-between px-3 md:px-6 h-8 md:h-9 border border-slate-200/40 group">
|
||||
<div className="flex items-center gap-2 md:gap-3 opacity-60 group-hover:opacity-100 transition-opacity duration-700 overflow-hidden">
|
||||
<ShieldCheck className="w-3 md:w-3.5 h-3 md:h-3.5 text-blue-500 shrink-0" />
|
||||
<span className="text-[9px] md:text-[10px] font-mono font-bold tracking-[0.1em] md:tracking-[0.2em] uppercase truncate whitespace-nowrap text-slate-900 md:border-r border-slate-200 md:pr-4 md:mr-1">
|
||||
{url.replace("varnish-cache://", "")}
|
||||
</span>
|
||||
<span className="text-[8px] font-mono text-slate-400 tracking-widest hidden xl:block">
|
||||
VARNISH_TUNNEL // ALPHA_NODE_04
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-green-400 animate-pulse hidden sm:block shadow-[0_0_8px_rgba(74,222,128,0.4)]" />
|
||||
</div>
|
||||
|
||||
{/* Industrial Accent */}
|
||||
<div className="flex items-center gap-2 opacity-30">
|
||||
<div className="w-8 h-1 bg-slate-400 rounded-full" />
|
||||
{/* Industrial Accent / Technical ID - Hidden on mobile */}
|
||||
<div className="hidden md:flex flex-col items-end opacity-20 pointer-events-none select-none">
|
||||
<div className="text-[6px] font-mono font-bold tracking-widest uppercase mb-0.5">
|
||||
SECURE_LINK // ID: VARNISH_4X
|
||||
</div>
|
||||
<div className="w-12 h-0.5 bg-slate-900 rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
BrowserChromeComponent.propTypes = {
|
||||
url: PropTypes.string.isRequired,
|
||||
minimal: PropTypes.bool,
|
||||
};
|
||||
|
||||
const BrowserChrome = React.memo(BrowserChromeComponent);
|
||||
|
||||
BrowserChrome.displayName = "BrowserChrome";
|
||||
|
||||
export const IframeSection: React.FC<IframeSectionProps> = ({
|
||||
src,
|
||||
title,
|
||||
@@ -78,12 +127,18 @@ export const IframeSection: React.FC<IframeSectionProps> = ({
|
||||
delay = 0,
|
||||
noScale = false,
|
||||
dynamicGlow = true,
|
||||
minHeight = 400,
|
||||
mobileWidth = 390,
|
||||
}) => {
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
const iframeRef = React.useRef<HTMLIFrameElement>(null);
|
||||
const canvasRef = React.useRef<HTMLCanvasElement>(null);
|
||||
const [scale, setScale] = React.useState(1);
|
||||
const [activeInternalWidth, setActiveInternalWidth] =
|
||||
React.useState(desktopWidth);
|
||||
const [isLoading, setIsLoading] = React.useState(true);
|
||||
const [isIframeLoaded, setIsIframeLoaded] = React.useState(false);
|
||||
const [isMinTimePassed, setIsMinTimePassed] = React.useState(false);
|
||||
const [glowColors, setGlowColors] = React.useState<string[]>([
|
||||
"rgba(148, 163, 184, 0.1)",
|
||||
"rgba(148, 163, 184, 0.1)",
|
||||
@@ -97,28 +152,52 @@ export const IframeSection: React.FC<IframeSectionProps> = ({
|
||||
isScrollable: false,
|
||||
});
|
||||
|
||||
// Scaling Logic
|
||||
const [headerHeightPx, setHeaderHeightPx] = React.useState(0);
|
||||
|
||||
// Responsive Header Height Calculation
|
||||
React.useEffect(() => {
|
||||
if (browserFrame && !minimal) {
|
||||
const updateHeaderHeight = () => {
|
||||
setHeaderHeightPx(window.innerWidth < 768 ? 48 : 56);
|
||||
};
|
||||
updateHeaderHeight();
|
||||
window.addEventListener("resize", updateHeaderHeight);
|
||||
return () => window.removeEventListener("resize", updateHeaderHeight);
|
||||
} else {
|
||||
setHeaderHeightPx(0);
|
||||
}
|
||||
}, [browserFrame, minimal]);
|
||||
|
||||
// Scaling & Adaptive Viewport Logic
|
||||
React.useEffect(() => {
|
||||
if (!containerRef.current || noScale) {
|
||||
setScale(1);
|
||||
setActiveInternalWidth(desktopWidth);
|
||||
return;
|
||||
}
|
||||
|
||||
const updateScale = () => {
|
||||
const updateDimensions = () => {
|
||||
if (containerRef.current) {
|
||||
const currentWidth = containerRef.current.offsetWidth;
|
||||
if (currentWidth > 0) {
|
||||
const newScale = zoom || currentWidth / desktopWidth;
|
||||
const containerWidth = containerRef.current.offsetWidth;
|
||||
if (containerWidth > 0) {
|
||||
// Adaptive threshold: Switch to mobile width if container is small
|
||||
const useMobile = containerWidth < 500;
|
||||
const internalWidth = useMobile ? mobileWidth : desktopWidth;
|
||||
|
||||
setActiveInternalWidth(internalWidth);
|
||||
|
||||
// Calculate scale based on the active target width
|
||||
const newScale = zoom || containerWidth / internalWidth;
|
||||
setScale(newScale);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
updateScale();
|
||||
const observer = new ResizeObserver(updateScale);
|
||||
updateDimensions();
|
||||
const observer = new ResizeObserver(updateDimensions);
|
||||
observer.observe(containerRef.current);
|
||||
return () => observer.disconnect();
|
||||
}, [desktopWidth, zoom, noScale]);
|
||||
}, [desktopWidth, mobileWidth, zoom, noScale]);
|
||||
|
||||
const updateScrollState = React.useCallback(() => {
|
||||
try {
|
||||
@@ -178,8 +257,6 @@ export const IframeSection: React.FC<IframeSectionProps> = ({
|
||||
} catch (e) {}
|
||||
}, [dynamicGlow, offsetY, updateScrollState]);
|
||||
|
||||
const headerHeightPx = browserFrame && !minimal ? 56 : 0;
|
||||
|
||||
// Height parse helper
|
||||
const parseNumericHeight = (h: string | number) => {
|
||||
if (typeof h === "number") return h;
|
||||
@@ -201,8 +278,57 @@ export const IframeSection: React.FC<IframeSectionProps> = ({
|
||||
: finalScaledHeight
|
||||
? `${finalScaledHeight + headerHeightPx}px`
|
||||
: `calc(${height} + ${headerHeightPx}px)`,
|
||||
minHeight: minHeight ? `${minHeight}px` : undefined,
|
||||
};
|
||||
|
||||
const [loadingPhase, setLoadingPhase] = React.useState(0);
|
||||
const loadingPhases = [
|
||||
"DETERMINING ROUTE",
|
||||
"INITIALIZING HANDSHAKE",
|
||||
"ESTABLISHING ENCRYPTED LINK",
|
||||
"SYNCING ASSETS",
|
||||
"FINALIZING...",
|
||||
];
|
||||
|
||||
// 1. Safety Trigger: Force-stop loading after 2.5s no matter what
|
||||
React.useEffect(() => {
|
||||
if (!isLoading) return;
|
||||
const timer = setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
}, 2500);
|
||||
return () => clearTimeout(timer);
|
||||
}, [isLoading]);
|
||||
|
||||
// 2. Sync Trigger: Cleanup when BOTH phases and iframe load complete
|
||||
React.useEffect(() => {
|
||||
if (!isLoading) return;
|
||||
|
||||
// Early exit: if iframe is already loaded, we only need the first 2 "handshake" phases
|
||||
const phasesRequired = isIframeLoaded
|
||||
? loadingPhases.length - 2
|
||||
: loadingPhases.length - 1;
|
||||
|
||||
if (loadingPhase >= phasesRequired) {
|
||||
const timer = setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
}, 300);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [isLoading, isIframeLoaded, loadingPhase, loadingPhases.length]);
|
||||
|
||||
// 3. Phase Incrementer (Faster: 400ms)
|
||||
React.useEffect(() => {
|
||||
if (!isLoading) return;
|
||||
const interval = setInterval(() => {
|
||||
setLoadingPhase((prev) => {
|
||||
if (prev < loadingPhases.length - 1) return prev + 1;
|
||||
clearInterval(interval);
|
||||
return prev;
|
||||
});
|
||||
}, 400);
|
||||
return () => clearInterval(interval);
|
||||
}, [isLoading, loadingPhases.length]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -231,11 +357,12 @@ export const IframeSection: React.FC<IframeSectionProps> = ({
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={cn(
|
||||
"w-full relative transition-[transform,shadow,scale] duration-1000 ease-[cubic-bezier(0.23,1,0.32,1)] flex flex-col z-10",
|
||||
"w-full relative flex flex-col z-10",
|
||||
minimal ? "bg-transparent" : "bg-slate-50",
|
||||
!minimal &&
|
||||
"rounded-[2.5rem] border border-slate-200/50 shadow-[0_80px_160px_-40px_rgba(0,0,0,0.18),0_0_1px_rgba(0,0,0,0.1)]",
|
||||
perspective && "hover:scale-[1.03] hover:-translate-y-3",
|
||||
perspective &&
|
||||
"hover:scale-[1.03] hover:-translate-y-3 transition-[transform,shadow,scale] duration-1000 ease-[cubic-bezier(0.23,1,0.32,1)]",
|
||||
"overflow-hidden",
|
||||
)}
|
||||
style={chassisStyle}
|
||||
@@ -266,94 +393,130 @@ export const IframeSection: React.FC<IframeSectionProps> = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Scaled Viewport Container */}
|
||||
<div className="flex-1 relative overflow-hidden bg-slate-50/50">
|
||||
{/* Loader Overlay - Now scoped to viewport */}
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-white/60 backdrop-blur-xl z-50 transition-opacity duration-700">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="w-12 h-12 border-[3px] border-slate-100 border-t-slate-900 rounded-full animate-spin" />
|
||||
<MonoLabel className="text-[10px] text-slate-400 animate-pulse uppercase tracking-[0.2em]">
|
||||
Establishing Connection
|
||||
</MonoLabel>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Scaled Viewport Container - Solid bg prevents gray flicker */}
|
||||
<div className="flex-1 relative overflow-hidden bg-white">
|
||||
{/* Artificial Loader Overlay */}
|
||||
<AnimatePresence>
|
||||
{isLoading && (
|
||||
<motion.div
|
||||
initial={{ opacity: 1 }}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
transition: { duration: 0.6, ease: [0.23, 1, 0.32, 1] },
|
||||
}}
|
||||
className="absolute inset-0 flex items-center justify-center bg-white/95 backdrop-blur-3xl z-50 pointer-events-none transform-gpu"
|
||||
>
|
||||
{/* Static Binary Matrix - Stop redundant re-paints */}
|
||||
<div
|
||||
className="absolute inset-0 opacity-[0.03] select-none pointer-events-none font-mono text-[8px] p-4 flex flex-col gap-1 overflow-hidden"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{Array.from({ length: 24 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="whitespace-nowrap tracking-[0.5em] opacity-50"
|
||||
>
|
||||
{"01011010010110101101011010101011010101011010101101010101101010"
|
||||
.split("")
|
||||
.join(" ")}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center gap-6 relative z-10 w-full max-w-xs px-8">
|
||||
<div className="w-full h-1 bg-slate-100 rounded-full overflow-hidden">
|
||||
<motion.div
|
||||
initial={{ width: 0 }}
|
||||
animate={{
|
||||
width: `${((loadingPhase + 1) / loadingPhases.length) * 100}%`,
|
||||
}}
|
||||
className="h-full bg-slate-900"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<MonoLabel className="text-[10px] text-slate-900 font-bold uppercase tracking-[0.3em] flex items-center gap-2">
|
||||
<span className="w-1 h-1 rounded-full bg-blue-500 animate-pulse" />
|
||||
{loadingPhases[loadingPhase]}
|
||||
</MonoLabel>
|
||||
<Label className="text-[8px] text-slate-400 font-mono uppercase tracking-widest">
|
||||
Varnish_Tunnel // Alpha_Node
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<div
|
||||
className={cn(
|
||||
"transition-all duration-1000 ease-[cubic-bezier(0.23,1,0.32,1)] origin-top-left absolute left-0 right-0",
|
||||
"transition-all duration-1000 ease-[cubic-bezier(0.23,1,0.32,1)] origin-top-left absolute left-0 right-0 transform-gpu",
|
||||
noScale && "relative w-full h-full",
|
||||
)}
|
||||
style={{
|
||||
width: noScale ? "100%" : `${desktopWidth}px`,
|
||||
width: noScale ? "100%" : `${activeInternalWidth}px`,
|
||||
transform: noScale ? "none" : `scale(${scale})`,
|
||||
height: noScale ? "100%" : `${100 / scale}%`,
|
||||
willChange: "opacity",
|
||||
}}
|
||||
>
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
src={src}
|
||||
scrolling={allowScroll ? "yes" : "no"}
|
||||
className={cn(
|
||||
"w-full border-none transition-all duration-700 no-scrollbar relative z-0",
|
||||
isLoading ? "opacity-0 scale-95" : "opacity-100 scale-100",
|
||||
)}
|
||||
onLoad={(e) => {
|
||||
setIsLoading(false);
|
||||
try {
|
||||
const iframe = e.currentTarget;
|
||||
if (iframe.contentDocument) {
|
||||
const style = iframe.contentDocument.createElement("style");
|
||||
style.textContent = `
|
||||
*::-webkit-scrollbar { display: none !important; }
|
||||
* { -ms-overflow-style: none !important; scrollbar-width: none !important; }
|
||||
body { background: transparent !important; }
|
||||
`;
|
||||
iframe.contentDocument.head.appendChild(style);
|
||||
setTimeout(updateAmbilight, 600);
|
||||
<motion.div
|
||||
initial={false}
|
||||
animate={{
|
||||
opacity: isLoading ? 0 : 1,
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.6,
|
||||
ease: [0.23, 1, 0.32, 1],
|
||||
}}
|
||||
className="w-full h-full relative z-0"
|
||||
>
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
src={src}
|
||||
scrolling={allowScroll ? "yes" : "no"}
|
||||
className="w-full h-full border-none no-scrollbar"
|
||||
onLoad={(e) => {
|
||||
setIsIframeLoaded(true);
|
||||
try {
|
||||
const iframe = e.currentTarget;
|
||||
if (iframe.contentDocument) {
|
||||
const style =
|
||||
iframe.contentDocument.createElement("style");
|
||||
style.textContent = `
|
||||
*::-webkit-scrollbar { display: none !important; }
|
||||
* { -ms-overflow-style: none !important; scrollbar-width: none !important; }
|
||||
body { background: transparent !important; }
|
||||
`;
|
||||
iframe.contentDocument.head.appendChild(style);
|
||||
setTimeout(updateAmbilight, 600);
|
||||
|
||||
const onScroll = () => {
|
||||
requestAnimationFrame(updateAmbilight);
|
||||
updateScrollState();
|
||||
};
|
||||
const onScroll = () => {
|
||||
requestAnimationFrame(updateAmbilight);
|
||||
updateScrollState();
|
||||
};
|
||||
|
||||
iframe.contentWindow?.addEventListener("scroll", onScroll, {
|
||||
passive: true,
|
||||
});
|
||||
iframe.contentWindow?.addEventListener(
|
||||
"scroll",
|
||||
onScroll,
|
||||
{
|
||||
passive: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (_e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
iframe.contentWindow?.addEventListener(
|
||||
"wheel",
|
||||
(e) => {
|
||||
const { deltaY } = e as WheelEvent;
|
||||
const doc = iframe.contentDocument?.documentElement;
|
||||
if (!doc) return;
|
||||
const scrollTop = doc.scrollTop;
|
||||
const isAtTop = scrollTop <= 0;
|
||||
const isAtBottom =
|
||||
scrollTop + doc.clientHeight >= doc.scrollHeight - 1;
|
||||
if (
|
||||
(isAtTop && deltaY < 0) ||
|
||||
(isAtBottom && deltaY > 0)
|
||||
) {
|
||||
window.scrollBy({ top: deltaY, behavior: "auto" });
|
||||
}
|
||||
},
|
||||
{ passive: true },
|
||||
);
|
||||
} catch (_e) {
|
||||
// ignore
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
transform: `translateY(-${offsetY}px)`,
|
||||
height: `calc(100% + ${offsetY}px)`,
|
||||
pointerEvents: allowScroll ? "auto" : "none",
|
||||
width: "calc(100% + 20px)", // Bleed for seamless edge
|
||||
marginLeft: "-10px",
|
||||
}}
|
||||
title={title || "Project Display"}
|
||||
/>
|
||||
}}
|
||||
style={{
|
||||
transform: `translateY(-${offsetY}px)`,
|
||||
height: `calc(100% + ${offsetY}px)`,
|
||||
pointerEvents: allowScroll ? "auto" : "none",
|
||||
width: "calc(100% + 20px)", // Bleed for seamless edge
|
||||
marginLeft: "-10px",
|
||||
}}
|
||||
title={title || "Project Display"}
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Custom Industrial Scroll Indicator */}
|
||||
@@ -380,10 +543,6 @@ export const IframeSection: React.FC<IframeSectionProps> = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!allowScroll && (
|
||||
<div className="absolute inset-x-0 bottom-0 top-14 pointer-events-auto cursor-default z-20" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user