chore: stabilize apps/web (lint, build, typecheck fixes)
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 6s
Build & Deploy / 🧪 QA (push) Failing after 1m27s
Build & Deploy / 🏗️ Build (push) Failing after 1m31s
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 6s
Build & Deploy / 🧪 QA (push) Failing after 1m27s
Build & Deploy / 🏗️ Build (push) Failing after 1m31s
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:
@@ -1,339 +1,390 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { cn } from '../utils/cn';
|
||||
import { ShieldCheck } from 'lucide-react';
|
||||
import { MonoLabel } from './Typography';
|
||||
/* eslint-disable no-unused-vars */
|
||||
import * as React from "react";
|
||||
import { cn } from "../utils/cn";
|
||||
import { ShieldCheck } from "lucide-react";
|
||||
import { MonoLabel } from "./Typography";
|
||||
|
||||
interface IframeSectionProps {
|
||||
src: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
height?: string;
|
||||
className?: string;
|
||||
zoom?: number;
|
||||
offsetY?: number;
|
||||
clipHeight?: number;
|
||||
browserFrame?: boolean;
|
||||
allowScroll?: boolean;
|
||||
desktopWidth?: number;
|
||||
minimal?: boolean;
|
||||
perspective?: boolean;
|
||||
rotate?: number;
|
||||
delay?: number;
|
||||
noScale?: boolean;
|
||||
dynamicGlow?: boolean;
|
||||
src: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
height?: string;
|
||||
className?: string;
|
||||
zoom?: number;
|
||||
offsetY?: number;
|
||||
clipHeight?: number;
|
||||
browserFrame?: boolean;
|
||||
allowScroll?: boolean;
|
||||
desktopWidth?: number;
|
||||
minimal?: boolean;
|
||||
perspective?: boolean;
|
||||
rotate?: number;
|
||||
delay?: number;
|
||||
noScale?: boolean;
|
||||
dynamicGlow?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable Browser UI components to maintain consistency
|
||||
*/
|
||||
const BrowserChrome: 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>
|
||||
const BrowserChrome: 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>
|
||||
|
||||
{/* 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}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Industrial Accent */}
|
||||
<div className="flex items-center gap-2 opacity-30">
|
||||
<div className="w-8 h-1 bg-slate-400 rounded-full" />
|
||||
</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}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
</div>
|
||||
|
||||
{/* Industrial Accent */}
|
||||
<div className="flex items-center gap-2 opacity-30">
|
||||
<div className="w-8 h-1 bg-slate-400 rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const IframeSection: React.FC<IframeSectionProps> = ({
|
||||
src,
|
||||
title,
|
||||
description,
|
||||
height = "500px",
|
||||
className,
|
||||
zoom,
|
||||
offsetY = 0,
|
||||
clipHeight,
|
||||
browserFrame = false,
|
||||
allowScroll = false,
|
||||
desktopWidth = 1200,
|
||||
minimal = false,
|
||||
perspective = false,
|
||||
rotate = 0,
|
||||
delay = 0,
|
||||
noScale = false,
|
||||
dynamicGlow = true
|
||||
src,
|
||||
title,
|
||||
description,
|
||||
height = "500px",
|
||||
className,
|
||||
zoom,
|
||||
offsetY = 0,
|
||||
clipHeight,
|
||||
browserFrame = false,
|
||||
allowScroll = false,
|
||||
desktopWidth = 1200,
|
||||
minimal = false,
|
||||
perspective = false,
|
||||
rotate = 0,
|
||||
delay = 0,
|
||||
noScale = false,
|
||||
dynamicGlow = true,
|
||||
}) => {
|
||||
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 [isLoading, setIsLoading] = React.useState(true);
|
||||
const [glowColors, setGlowColors] = React.useState<string[]>([
|
||||
'rgba(148, 163, 184, 0.1)',
|
||||
'rgba(148, 163, 184, 0.1)',
|
||||
'rgba(148, 163, 184, 0.1)',
|
||||
'rgba(148, 163, 184, 0.1)'
|
||||
]);
|
||||
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 [isLoading, setIsLoading] = React.useState(true);
|
||||
const [glowColors, setGlowColors] = React.useState<string[]>([
|
||||
"rgba(148, 163, 184, 0.1)",
|
||||
"rgba(148, 163, 184, 0.1)",
|
||||
"rgba(148, 163, 184, 0.1)",
|
||||
"rgba(148, 163, 184, 0.1)",
|
||||
]);
|
||||
|
||||
const [scrollState, setScrollState] = React.useState({ atTop: true, atBottom: false, isScrollable: false });
|
||||
const [scrollState, setScrollState] = React.useState({
|
||||
atTop: true,
|
||||
atBottom: false,
|
||||
isScrollable: false,
|
||||
});
|
||||
|
||||
// Scaling Logic
|
||||
React.useEffect(() => {
|
||||
if (!containerRef.current || noScale) {
|
||||
setScale(1);
|
||||
return;
|
||||
// Scaling Logic
|
||||
React.useEffect(() => {
|
||||
if (!containerRef.current || noScale) {
|
||||
setScale(1);
|
||||
return;
|
||||
}
|
||||
|
||||
const updateScale = () => {
|
||||
if (containerRef.current) {
|
||||
const currentWidth = containerRef.current.offsetWidth;
|
||||
if (currentWidth > 0) {
|
||||
const newScale = zoom || currentWidth / desktopWidth;
|
||||
setScale(newScale);
|
||||
}
|
||||
|
||||
const updateScale = () => {
|
||||
if (containerRef.current) {
|
||||
const currentWidth = containerRef.current.offsetWidth;
|
||||
if (currentWidth > 0) {
|
||||
const newScale = zoom || (currentWidth / desktopWidth);
|
||||
setScale(newScale);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
updateScale();
|
||||
const observer = new ResizeObserver(updateScale);
|
||||
observer.observe(containerRef.current);
|
||||
return () => observer.disconnect();
|
||||
}, [desktopWidth, zoom, noScale]);
|
||||
|
||||
const updateScrollState = React.useCallback(() => {
|
||||
try {
|
||||
const doc = iframeRef.current?.contentDocument?.documentElement;
|
||||
if (doc) {
|
||||
const atTop = doc.scrollTop <= 5;
|
||||
const atBottom = doc.scrollTop + doc.clientHeight >= doc.scrollHeight - 5;
|
||||
const isScrollable = doc.scrollHeight > doc.clientHeight + 10;
|
||||
setScrollState({ atTop, atBottom, isScrollable });
|
||||
}
|
||||
} catch (e) { }
|
||||
}, []);
|
||||
|
||||
// Ambilight effect (sampled from iframe if same-origin)
|
||||
const updateAmbilight = React.useCallback(() => {
|
||||
if (!dynamicGlow || !iframeRef.current || !canvasRef.current) return;
|
||||
try {
|
||||
const iframe = iframeRef.current;
|
||||
const doc = iframe.contentDocument;
|
||||
if (!doc) return;
|
||||
const canvas = canvasRef.current;
|
||||
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
||||
if (!ctx) return;
|
||||
|
||||
canvas.width = 100;
|
||||
canvas.height = 100;
|
||||
|
||||
const body = doc.body;
|
||||
const computedStyle = window.getComputedStyle(body);
|
||||
const bgColor = computedStyle.backgroundColor || 'rgba(255,255,255,1)';
|
||||
|
||||
const sampleX = (x: number, y: number) => {
|
||||
const el = doc.elementFromPoint(x, y);
|
||||
if (el) return window.getComputedStyle(el).backgroundColor;
|
||||
return bgColor;
|
||||
};
|
||||
|
||||
const w = doc.documentElement.scrollWidth || iframe.offsetWidth;
|
||||
const h = doc.documentElement.scrollHeight || iframe.offsetHeight;
|
||||
const sampleMargin = 20;
|
||||
const colors = [
|
||||
sampleX(w / 2, sampleMargin + offsetY),
|
||||
sampleX(w - sampleMargin, h / 2 + offsetY),
|
||||
sampleX(w / 2, h - sampleMargin + offsetY),
|
||||
sampleX(sampleMargin, h / 2 + offsetY)
|
||||
];
|
||||
|
||||
setGlowColors(colors.map(c => {
|
||||
if (!c || c === 'transparent') return 'rgba(148, 163, 184, 0.1)';
|
||||
return c.replace('rgb(', 'rgba(').replace(')', ', 0.5)');
|
||||
}));
|
||||
|
||||
updateScrollState();
|
||||
} 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;
|
||||
const match = h.match(/^(\d+(?:\.\d+)?)(px)$/);
|
||||
return match ? parseFloat(match[1]) : null;
|
||||
}
|
||||
};
|
||||
|
||||
const baseNumericHeight = parseNumericHeight(height);
|
||||
const finalScaledHeight = clipHeight
|
||||
? (clipHeight * scale)
|
||||
: (baseNumericHeight ? (baseNumericHeight * scale) : null);
|
||||
updateScale();
|
||||
const observer = new ResizeObserver(updateScale);
|
||||
observer.observe(containerRef.current);
|
||||
return () => observer.disconnect();
|
||||
}, [desktopWidth, zoom, noScale]);
|
||||
|
||||
const chassisStyle = {
|
||||
height: height === '100%'
|
||||
? '100%'
|
||||
: (finalScaledHeight ? `${finalScaledHeight + headerHeightPx}px` : `calc(${height} + ${headerHeightPx}px)`)
|
||||
};
|
||||
const updateScrollState = React.useCallback(() => {
|
||||
try {
|
||||
const doc = iframeRef.current?.contentDocument?.documentElement;
|
||||
if (doc) {
|
||||
const atTop = doc.scrollTop <= 5;
|
||||
const atBottom =
|
||||
doc.scrollTop + doc.clientHeight >= doc.scrollHeight - 5;
|
||||
const isScrollable = doc.scrollHeight > doc.clientHeight + 10;
|
||||
setScrollState({ atTop, atBottom, isScrollable });
|
||||
}
|
||||
} catch (e) {}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("w-full group relative", !minimal && "space-y-6", className)}
|
||||
style={className?.includes('h-full') ? { height: '100%' } : {}}
|
||||
>
|
||||
<canvas ref={canvasRef} className="hidden" aria-hidden="true" />
|
||||
// Ambilight effect (sampled from iframe if same-origin)
|
||||
const updateAmbilight = React.useCallback(() => {
|
||||
if (!dynamicGlow || !iframeRef.current || !canvasRef.current) return;
|
||||
try {
|
||||
const iframe = iframeRef.current;
|
||||
const doc = iframe.contentDocument;
|
||||
if (!doc) return;
|
||||
const canvas = canvasRef.current;
|
||||
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
||||
if (!ctx) return;
|
||||
|
||||
{!minimal && (title || description) && (
|
||||
<div className="space-y-2 px-1">
|
||||
{title && <h4 className="text-2xl font-bold text-slate-900 tracking-tight leading-none">{title}</h4>}
|
||||
{description && <p className="text-slate-400 text-sm font-medium">{description}</p>}
|
||||
</div>
|
||||
)}
|
||||
canvas.width = 100;
|
||||
canvas.height = 100;
|
||||
|
||||
{/* Main Device Chassis */}
|
||||
const body = doc.body;
|
||||
const computedStyle = window.getComputedStyle(body);
|
||||
const bgColor = computedStyle.backgroundColor || "rgba(255,255,255,1)";
|
||||
|
||||
const sampleX = (x: number, y: number) => {
|
||||
const el = doc.elementFromPoint(x, y);
|
||||
if (el) return window.getComputedStyle(el).backgroundColor;
|
||||
return bgColor;
|
||||
};
|
||||
|
||||
const w = doc.documentElement.scrollWidth || iframe.offsetWidth;
|
||||
const h = doc.documentElement.scrollHeight || iframe.offsetHeight;
|
||||
const sampleMargin = 20;
|
||||
const colors = [
|
||||
sampleX(w / 2, sampleMargin + offsetY),
|
||||
sampleX(w - sampleMargin, h / 2 + offsetY),
|
||||
sampleX(w / 2, h - sampleMargin + offsetY),
|
||||
sampleX(sampleMargin, h / 2 + offsetY),
|
||||
];
|
||||
|
||||
setGlowColors(
|
||||
colors.map((c) => {
|
||||
if (!c || c === "transparent") return "rgba(148, 163, 184, 0.1)";
|
||||
return c.replace("rgb(", "rgba(").replace(")", ", 0.5)");
|
||||
}),
|
||||
);
|
||||
|
||||
updateScrollState();
|
||||
} 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;
|
||||
const match = h.match(/^(\d+(?:\.\d+)?)(px)$/);
|
||||
return match ? parseFloat(match[1]) : null;
|
||||
};
|
||||
|
||||
const baseNumericHeight = parseNumericHeight(height);
|
||||
const finalScaledHeight = clipHeight
|
||||
? clipHeight * scale
|
||||
: baseNumericHeight
|
||||
? baseNumericHeight * scale
|
||||
: null;
|
||||
|
||||
const chassisStyle = {
|
||||
height:
|
||||
height === "100%"
|
||||
? "100%"
|
||||
: finalScaledHeight
|
||||
? `${finalScaledHeight + headerHeightPx}px`
|
||||
: `calc(${height} + ${headerHeightPx}px)`,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"w-full group relative",
|
||||
!minimal && "space-y-6",
|
||||
className,
|
||||
)}
|
||||
style={className?.includes("h-full") ? { height: "100%" } : {}}
|
||||
>
|
||||
<canvas ref={canvasRef} className="hidden" aria-hidden="true" />
|
||||
|
||||
{!minimal && (title || description) && (
|
||||
<div className="space-y-2 px-1">
|
||||
{title && (
|
||||
<h4 className="text-2xl font-bold text-slate-900 tracking-tight leading-none">
|
||||
{title}
|
||||
</h4>
|
||||
)}
|
||||
{description && (
|
||||
<p className="text-slate-400 text-sm font-medium">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main Device Chassis */}
|
||||
<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",
|
||||
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",
|
||||
"overflow-hidden",
|
||||
)}
|
||||
style={chassisStyle}
|
||||
>
|
||||
{/* AMBILIGHT DYNAMIC GLOW */}
|
||||
{dynamicGlow && (
|
||||
<div className="absolute -inset-[30%] blur-[140px] opacity-30 group-hover:opacity-90 transition-all duration-[2000ms] pointer-events-none z-0">
|
||||
<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",
|
||||
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",
|
||||
"overflow-hidden"
|
||||
)}
|
||||
style={chassisStyle}
|
||||
>
|
||||
{/* AMBILIGHT DYNAMIC GLOW */}
|
||||
{dynamicGlow && (
|
||||
<div className="absolute -inset-[30%] blur-[140px] opacity-30 group-hover:opacity-90 transition-all duration-[2000ms] pointer-events-none z-0">
|
||||
<div
|
||||
className="absolute inset-0 rounded-[6rem]"
|
||||
style={{
|
||||
background: `
|
||||
className="absolute inset-0 rounded-[6rem]"
|
||||
style={{
|
||||
background: `
|
||||
radial-gradient(circle at 50% 10%, ${glowColors[0]} 0%, transparent 60%),
|
||||
radial-gradient(circle at 95% 50%, ${glowColors[1]} 0%, transparent 60%),
|
||||
radial-gradient(circle at 50% 90%, ${glowColors[2]} 0%, transparent 60%),
|
||||
radial-gradient(circle at 5% 50%, ${glowColors[3]} 0%, transparent 60%)
|
||||
`,
|
||||
filter: 'saturate(2.2) brightness(1.1)'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
filter: "saturate(2.2) brightness(1.1)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Browser Frame */}
|
||||
{browserFrame && <BrowserChrome url="varnish-cache://secure.klz-cables.com" minimal={minimal} />}
|
||||
{/* Browser Frame */}
|
||||
{browserFrame && (
|
||||
<BrowserChrome
|
||||
url="varnish-cache://secure.klz-cables.com"
|
||||
minimal={minimal}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"transition-all duration-1000 ease-[cubic-bezier(0.23,1,0.32,1)] origin-top-left absolute left-0 right-0",
|
||||
noScale && "relative w-full h-full"
|
||||
)}
|
||||
style={{
|
||||
width: noScale ? '100%' : `${desktopWidth}px`,
|
||||
transform: noScale ? 'none' : `scale(${scale})`,
|
||||
height: noScale ? '100%' : `${100 / scale}%`,
|
||||
}}
|
||||
>
|
||||
<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 = `
|
||||
{/* 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>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"transition-all duration-1000 ease-[cubic-bezier(0.23,1,0.32,1)] origin-top-left absolute left-0 right-0",
|
||||
noScale && "relative w-full h-full",
|
||||
)}
|
||||
style={{
|
||||
width: noScale ? "100%" : `${desktopWidth}px`,
|
||||
transform: noScale ? "none" : `scale(${scale})`,
|
||||
height: noScale ? "100%" : `${100 / scale}%`,
|
||||
}}
|
||||
>
|
||||
<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);
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
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 (err) { }
|
||||
}}
|
||||
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"}
|
||||
/>
|
||||
</div>
|
||||
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"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Custom Industrial Scroll Indicator */}
|
||||
{allowScroll && scrollState.isScrollable && (
|
||||
<div className="absolute right-4 top-1/2 -translate-y-1/2 w-1 h-32 bg-slate-200/20 rounded-full z-20 backdrop-blur-sm">
|
||||
<div
|
||||
className="w-full bg-slate-900 rounded-full transition-all duration-150 ease-out shadow-[0_0_12px_rgba(15,23,42,0.1)]"
|
||||
style={{
|
||||
height: '30px',
|
||||
transform: `translateY(${(() => {
|
||||
try {
|
||||
const doc = iframeRef.current?.contentDocument?.documentElement;
|
||||
if (!doc) return 0;
|
||||
const scrollPct = doc.scrollTop / (doc.scrollHeight - doc.clientHeight);
|
||||
return scrollPct * (128 - 30);
|
||||
} catch (e) { return 0; }
|
||||
})()}px)`
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!allowScroll && <div className="absolute inset-x-0 bottom-0 top-14 pointer-events-auto cursor-default z-20" />}
|
||||
{/* Custom Industrial Scroll Indicator */}
|
||||
{allowScroll && scrollState.isScrollable && (
|
||||
<div className="absolute right-4 top-1/2 -translate-y-1/2 w-1 h-32 bg-slate-200/20 rounded-full z-20 backdrop-blur-sm">
|
||||
<div
|
||||
className="w-full bg-slate-900 rounded-full transition-all duration-150 ease-out shadow-[0_0_12px_rgba(15,23,42,0.1)]"
|
||||
style={{
|
||||
height: "30px",
|
||||
transform: `translateY(${(() => {
|
||||
try {
|
||||
const doc =
|
||||
iframeRef.current?.contentDocument?.documentElement;
|
||||
if (!doc) return 0;
|
||||
const scrollPct =
|
||||
doc.scrollTop / (doc.scrollHeight - doc.clientHeight);
|
||||
return scrollPct * (128 - 30);
|
||||
} catch (e) {
|
||||
return 0;
|
||||
}
|
||||
})()}px)`,
|
||||
}}
|
||||
/>
|
||||
</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