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:
181
apps/web/src/components/Effects/AbstractCircuit.tsx
Normal file
181
apps/web/src/components/Effects/AbstractCircuit.tsx
Normal file
@@ -0,0 +1,181 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
/**
|
||||
* AbstractCircuit: Premium Canvas Binary Data Flow
|
||||
*
|
||||
* - Binary 0/1 characters flow in structured horizontal lanes
|
||||
* - Vertical cross-traffic for depth
|
||||
* - Characters "breathe" with sine-wave opacity
|
||||
* - High performance: uses requestAnimationFrame, minimal allocations per frame
|
||||
*/
|
||||
|
||||
interface Char {
|
||||
x: number;
|
||||
y: number;
|
||||
val: number; // 0 or 1
|
||||
speed: number; // px per frame
|
||||
vertical: boolean;
|
||||
size: number;
|
||||
baseAlpha: number;
|
||||
phase: number; // sine-wave phase offset
|
||||
flipIn: number; // frames until next flip
|
||||
}
|
||||
|
||||
export const AbstractCircuit: React.FC<{
|
||||
invert?: boolean;
|
||||
className?: string;
|
||||
}> = ({ invert = false, className = "" }) => {
|
||||
const canvasRef = React.useRef<HTMLCanvasElement>(null);
|
||||
const stateRef = React.useRef({
|
||||
chars: [] as Char[],
|
||||
frame: 0,
|
||||
dpr: 1,
|
||||
w: 0,
|
||||
h: 0,
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext("2d", { alpha: true })!;
|
||||
const s = stateRef.current;
|
||||
let raf = 0;
|
||||
|
||||
// ── Sizing ──
|
||||
const resize = () => {
|
||||
s.dpr = Math.min(window.devicePixelRatio || 1, 2);
|
||||
s.w = canvas.offsetWidth;
|
||||
s.h = canvas.offsetHeight;
|
||||
canvas.width = s.w * s.dpr;
|
||||
canvas.height = s.h * s.dpr;
|
||||
seed();
|
||||
};
|
||||
|
||||
// ── Seed characters ──
|
||||
const LANE = 28;
|
||||
const GAP = 20;
|
||||
|
||||
const seed = () => {
|
||||
const chars: Char[] = [];
|
||||
const { w, h } = s;
|
||||
|
||||
// Horizontal lanes
|
||||
for (let y = 0; y < h; y += LANE) {
|
||||
const dir = Math.floor(y / LANE) % 3 === 0 ? -1 : 1;
|
||||
const spd = (0.15 + Math.random() * 0.6) * dir;
|
||||
for (let x = 0; x < w + GAP; x += GAP) {
|
||||
if (Math.random() > 0.45) continue;
|
||||
chars.push({
|
||||
x: x + (Math.random() - 0.5) * 6,
|
||||
y: y + LANE / 2 + (Math.random() - 0.5) * 4,
|
||||
val: Math.random() > 0.5 ? 1 : 0,
|
||||
speed: spd,
|
||||
vertical: false,
|
||||
size: 9 + Math.floor(Math.random() * 2),
|
||||
baseAlpha: 0.035 + Math.random() * 0.045,
|
||||
phase: Math.random() * Math.PI * 2,
|
||||
flipIn: 120 + Math.floor(Math.random() * 400),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Vertical lanes (sparser)
|
||||
for (let x = 0; x < w; x += LANE * 2.5) {
|
||||
const dir = Math.floor(x / LANE) % 2 === 0 ? 1 : -1;
|
||||
const spd = (0.1 + Math.random() * 0.4) * dir;
|
||||
for (let y = 0; y < h + GAP; y += GAP) {
|
||||
if (Math.random() > 0.3) continue;
|
||||
chars.push({
|
||||
x: x + (Math.random() - 0.5) * 4,
|
||||
y: y + (Math.random() - 0.5) * 6,
|
||||
val: Math.random() > 0.5 ? 1 : 0,
|
||||
speed: spd,
|
||||
vertical: true,
|
||||
size: 8 + Math.floor(Math.random() * 2),
|
||||
baseAlpha: 0.025 + Math.random() * 0.035,
|
||||
phase: Math.random() * Math.PI * 2,
|
||||
flipIn: 150 + Math.floor(Math.random() * 500),
|
||||
});
|
||||
}
|
||||
}
|
||||
s.chars = chars;
|
||||
};
|
||||
|
||||
// ── Mouse tracking — use canvas-relative coordinates ──
|
||||
|
||||
// ── Render loop ──
|
||||
const render = () => {
|
||||
const { w, h, dpr, chars } = s;
|
||||
s.frame++;
|
||||
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
const t = s.frame * 0.02; // global time
|
||||
|
||||
// ── Draw characters ──
|
||||
for (let i = 0, len = chars.length; i < len; i++) {
|
||||
const c = chars[i];
|
||||
|
||||
// Move
|
||||
if (c.vertical) {
|
||||
c.y += c.speed;
|
||||
if (c.y > h + 15) c.y = -15;
|
||||
else if (c.y < -15) c.y = h + 15;
|
||||
} else {
|
||||
c.x += c.speed;
|
||||
if (c.x > w + 15) c.x = -15;
|
||||
else if (c.x < -15) c.x = w + 15;
|
||||
}
|
||||
|
||||
// Flip timer
|
||||
if (--c.flipIn <= 0) {
|
||||
c.val ^= 1;
|
||||
c.flipIn = 120 + Math.floor(Math.random() * 400);
|
||||
}
|
||||
|
||||
// Sine-wave "breathing"
|
||||
const breath = Math.sin(t + c.phase) * 0.015;
|
||||
|
||||
let alpha = c.baseAlpha + breath;
|
||||
|
||||
// ── Draw ──
|
||||
const sz = c.size;
|
||||
ctx.font = `bold ${sz}px "SF Mono", "Fira Code", "Cascadia Code", "Menlo", monospace`;
|
||||
|
||||
ctx.fillStyle = invert
|
||||
? `rgba(255,255,255,${Math.max(alpha, 0)})`
|
||||
: `rgba(100,116,139,${Math.max(alpha, 0)})`;
|
||||
ctx.shadowColor = "transparent";
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
ctx.fillText(c.val ? "1" : "0", c.x, c.y);
|
||||
}
|
||||
|
||||
raf = requestAnimationFrame(render);
|
||||
};
|
||||
|
||||
// ── Event listeners ──
|
||||
// Listen on the PARENT element (the section) to capture all mouse movement
|
||||
window.addEventListener("resize", resize);
|
||||
|
||||
resize();
|
||||
raf = requestAnimationFrame(render);
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(raf);
|
||||
window.removeEventListener("resize", resize);
|
||||
};
|
||||
}, [invert]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`absolute inset-0 overflow-hidden ${className}`}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<canvas ref={canvasRef} className="absolute inset-0 w-full h-full" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
63
apps/web/src/components/Effects/BinaryStream.tsx
Normal file
63
apps/web/src/components/Effects/BinaryStream.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
interface BinaryStreamProps {
|
||||
className?: string;
|
||||
columns?: number;
|
||||
side?: "left" | "right" | "both";
|
||||
}
|
||||
|
||||
export const BinaryStream: React.FC<BinaryStreamProps> = ({
|
||||
className = "",
|
||||
columns = 4,
|
||||
side = "both",
|
||||
}) => {
|
||||
// Generate deterministic binary strings
|
||||
const generateColumn = (seed: number) => {
|
||||
const chars: string[] = [];
|
||||
for (let i = 0; i < 60; i++) {
|
||||
chars.push(((seed * 137 + i * 31) % 2).toString());
|
||||
}
|
||||
return chars.join(" ");
|
||||
};
|
||||
|
||||
const renderColumns = (position: "left" | "right") => (
|
||||
<div
|
||||
className={`absolute top-0 ${position === "left" ? "left-0" : "right-0"} flex gap-3 pointer-events-none select-none overflow-hidden`}
|
||||
style={{ height: "100%", width: `${columns * 16}px` }}
|
||||
>
|
||||
{Array.from({ length: columns }).map((_, i) => {
|
||||
const offset = position === "left" ? i : i + columns;
|
||||
const duration = 20 + (i % 3) * 8;
|
||||
const delay = i * 2.5;
|
||||
return (
|
||||
<div
|
||||
key={`${position}-${i}`}
|
||||
className="binary-column text-[10px] font-mono leading-[1.6] whitespace-pre tracking-widest"
|
||||
style={{
|
||||
color: "rgba(203, 213, 225, 0.12)",
|
||||
writingMode: "vertical-lr",
|
||||
animation: `binary-scroll ${duration}s linear ${delay}s infinite`,
|
||||
animationFillMode: "backwards",
|
||||
}}
|
||||
>
|
||||
{generateColumn(offset + 42)}
|
||||
{"\n"}
|
||||
{generateColumn(offset + 99)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`absolute inset-0 pointer-events-none overflow-hidden ${className}`}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{(side === "left" || side === "both") && renderColumns("left")}
|
||||
{(side === "right" || side === "both") && renderColumns("right")}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
317
apps/web/src/components/Effects/CircuitBoard.tsx
Normal file
317
apps/web/src/components/Effects/CircuitBoard.tsx
Normal file
@@ -0,0 +1,317 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { useEffect, useRef, useCallback } from "react";
|
||||
|
||||
interface Node {
|
||||
x: number;
|
||||
y: number;
|
||||
connections: number[];
|
||||
pulsePhase: number;
|
||||
pulseSpeed: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
interface Trace {
|
||||
from: number;
|
||||
to: number;
|
||||
progress: number;
|
||||
speed: number;
|
||||
active: boolean;
|
||||
delay: number;
|
||||
}
|
||||
|
||||
interface CircuitBoardProps {
|
||||
className?: string;
|
||||
density?: "low" | "medium" | "high";
|
||||
animate?: boolean;
|
||||
}
|
||||
|
||||
export const CircuitBoard: React.FC<CircuitBoardProps> = ({
|
||||
className = "",
|
||||
density = "medium",
|
||||
animate = true,
|
||||
}) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const nodesRef = useRef<Node[]>([]);
|
||||
const tracesRef = useRef<Trace[]>([]);
|
||||
const animationFrameRef = useRef<number>(0);
|
||||
const timeRef = useRef<number>(0);
|
||||
const dimensionsRef = useRef<{ width: number; height: number }>({
|
||||
width: 0,
|
||||
height: 0,
|
||||
});
|
||||
|
||||
const densityMap = { low: 12, medium: 20, high: 30 };
|
||||
|
||||
const initCircuit = useCallback(
|
||||
(width: number, height: number) => {
|
||||
const nodeCount = densityMap[density];
|
||||
const nodes: Node[] = [];
|
||||
const traces: Trace[] = [];
|
||||
const gridCols = Math.ceil(Math.sqrt(nodeCount * (width / height)));
|
||||
const gridRows = Math.ceil(nodeCount / gridCols);
|
||||
const cellW = width / gridCols;
|
||||
const cellH = height / gridRows;
|
||||
|
||||
// Create nodes on a jittered grid
|
||||
for (let row = 0; row < gridRows; row++) {
|
||||
for (let col = 0; col < gridCols; col++) {
|
||||
if (nodes.length >= nodeCount) break;
|
||||
nodes.push({
|
||||
x: cellW * (col + 0.3 + Math.random() * 0.4),
|
||||
y: cellH * (row + 0.3 + Math.random() * 0.4),
|
||||
connections: [],
|
||||
pulsePhase: Math.random() * Math.PI * 2,
|
||||
pulseSpeed: 0.5 + Math.random() * 1.5,
|
||||
size: 1.5 + Math.random() * 2,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Connect nearby nodes with orthogonal traces (PCB style)
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const maxConnections = 2 + Math.floor(Math.random() * 2);
|
||||
const distances: { idx: number; dist: number }[] = [];
|
||||
|
||||
for (let j = 0; j < nodes.length; j++) {
|
||||
if (i === j) continue;
|
||||
const dx = nodes[j].x - nodes[i].x;
|
||||
const dy = nodes[j].y - nodes[i].y;
|
||||
distances.push({ idx: j, dist: Math.sqrt(dx * dx + dy * dy) });
|
||||
}
|
||||
|
||||
distances.sort((a, b) => a.dist - b.dist);
|
||||
let connected = 0;
|
||||
|
||||
for (const d of distances) {
|
||||
if (connected >= maxConnections) break;
|
||||
if (d.dist > Math.max(cellW, cellH) * 2) break;
|
||||
|
||||
// Avoid duplicate traces
|
||||
const exists = traces.some(
|
||||
(t) =>
|
||||
(t.from === i && t.to === d.idx) ||
|
||||
(t.from === d.idx && t.to === i),
|
||||
);
|
||||
if (exists) continue;
|
||||
|
||||
nodes[i].connections.push(d.idx);
|
||||
nodes[d.idx].connections.push(i);
|
||||
traces.push({
|
||||
from: i,
|
||||
to: d.idx,
|
||||
progress: 0,
|
||||
speed: 0.002 + Math.random() * 0.004,
|
||||
active: Math.random() > 0.6,
|
||||
delay: Math.random() * 3000,
|
||||
});
|
||||
connected++;
|
||||
}
|
||||
}
|
||||
|
||||
nodesRef.current = nodes;
|
||||
tracesRef.current = traces;
|
||||
},
|
||||
[density],
|
||||
);
|
||||
|
||||
const drawTrace = useCallback(
|
||||
(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
x1: number,
|
||||
y1: number,
|
||||
x2: number,
|
||||
y2: number,
|
||||
alpha: number,
|
||||
) => {
|
||||
// Draw orthogonal PCB-style trace (L-shaped)
|
||||
const midX = Math.random() > 0.5 ? x2 : x1;
|
||||
const midY = Math.random() > 0.5 ? y1 : y2;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x1, y1);
|
||||
if (Math.abs(x2 - x1) > Math.abs(y2 - y1)) {
|
||||
ctx.lineTo(x2, y1);
|
||||
ctx.lineTo(x2, y2);
|
||||
} else {
|
||||
ctx.lineTo(x1, y2);
|
||||
ctx.lineTo(x2, y2);
|
||||
}
|
||||
ctx.strokeStyle = `rgba(203, 213, 225, ${alpha})`;
|
||||
ctx.lineWidth = 0.5;
|
||||
ctx.stroke();
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const animateFrame = useCallback(() => {
|
||||
const canvas = canvasRef.current;
|
||||
const ctx = canvas?.getContext("2d");
|
||||
if (!canvas || !ctx) return;
|
||||
|
||||
const { width, height } = dimensionsRef.current;
|
||||
const nodes = nodesRef.current;
|
||||
const traces = tracesRef.current;
|
||||
timeRef.current += 16;
|
||||
const time = timeRef.current;
|
||||
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
// Draw static traces
|
||||
traces.forEach((trace) => {
|
||||
const from = nodes[trace.from];
|
||||
const to = nodes[trace.to];
|
||||
if (!from || !to) return;
|
||||
|
||||
// Static trace line
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(from.x, from.y);
|
||||
if (Math.abs(to.x - from.x) > Math.abs(to.y - from.y)) {
|
||||
ctx.lineTo(to.x, from.y);
|
||||
ctx.lineTo(to.x, to.y);
|
||||
} else {
|
||||
ctx.lineTo(from.x, to.y);
|
||||
ctx.lineTo(to.x, to.y);
|
||||
}
|
||||
ctx.strokeStyle = "rgba(226, 232, 240, 0.4)";
|
||||
ctx.lineWidth = 0.5;
|
||||
ctx.stroke();
|
||||
|
||||
// Animated data packet traveling along trace
|
||||
if (animate && trace.active && time > trace.delay) {
|
||||
trace.progress += trace.speed;
|
||||
if (trace.progress > 1) {
|
||||
trace.progress = 0;
|
||||
trace.active = Math.random() > 0.3;
|
||||
trace.delay = time + Math.random() * 5000;
|
||||
}
|
||||
|
||||
const p = trace.progress;
|
||||
let px: number, py: number;
|
||||
|
||||
if (Math.abs(to.x - from.x) > Math.abs(to.y - from.y)) {
|
||||
if (p < 0.5) {
|
||||
px = from.x + (to.x - from.x) * (p * 2);
|
||||
py = from.y;
|
||||
} else {
|
||||
px = to.x;
|
||||
py = from.y + (to.y - from.y) * ((p - 0.5) * 2);
|
||||
}
|
||||
} else {
|
||||
if (p < 0.5) {
|
||||
px = from.x;
|
||||
py = from.y + (to.y - from.y) * (p * 2);
|
||||
} else {
|
||||
px = from.x + (to.x - from.x) * ((p - 0.5) * 2);
|
||||
py = to.y;
|
||||
}
|
||||
}
|
||||
|
||||
// Data packet glow
|
||||
const gradient = ctx.createRadialGradient(px, py, 0, px, py, 8);
|
||||
gradient.addColorStop(0, "rgba(148, 163, 184, 0.6)");
|
||||
gradient.addColorStop(1, "rgba(148, 163, 184, 0)");
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(px - 8, py - 8, 16, 16);
|
||||
|
||||
// Data packet dot
|
||||
ctx.beginPath();
|
||||
ctx.arc(px, py, 1.5, 0, Math.PI * 2);
|
||||
ctx.fillStyle = "rgba(148, 163, 184, 0.8)";
|
||||
ctx.fill();
|
||||
}
|
||||
});
|
||||
|
||||
// Draw nodes
|
||||
nodes.forEach((node) => {
|
||||
const pulse = animate
|
||||
? 0.3 + Math.sin(time * 0.001 * node.pulseSpeed + node.pulsePhase) * 0.2
|
||||
: 0.4;
|
||||
|
||||
// Node glow
|
||||
const gradient = ctx.createRadialGradient(
|
||||
node.x,
|
||||
node.y,
|
||||
0,
|
||||
node.x,
|
||||
node.y,
|
||||
node.size * 4,
|
||||
);
|
||||
gradient.addColorStop(0, `rgba(191, 203, 219, ${pulse * 0.3})`);
|
||||
gradient.addColorStop(1, "rgba(191, 203, 219, 0)");
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(
|
||||
node.x - node.size * 4,
|
||||
node.y - node.size * 4,
|
||||
node.size * 8,
|
||||
node.size * 8,
|
||||
);
|
||||
|
||||
// Node dot
|
||||
ctx.beginPath();
|
||||
ctx.arc(node.x, node.y, node.size, 0, Math.PI * 2);
|
||||
ctx.fillStyle = `rgba(203, 213, 225, ${pulse + 0.2})`;
|
||||
ctx.fill();
|
||||
});
|
||||
|
||||
// Draw subtle binary text near some nodes
|
||||
if (animate) {
|
||||
ctx.font = "9px ui-monospace, monospace";
|
||||
nodes.forEach((node, i) => {
|
||||
if (i % 4 !== 0) return; // Only every 4th node
|
||||
const binaryAlpha =
|
||||
0.06 + Math.sin(time * 0.0008 + node.pulsePhase) * 0.04;
|
||||
ctx.fillStyle = `rgba(148, 163, 184, ${binaryAlpha})`;
|
||||
const binary = ((time * 0.01 + i * 137) % 256)
|
||||
.toString(2)
|
||||
.padStart(8, "0");
|
||||
ctx.fillText(binary, node.x + node.size * 3, node.y + 3);
|
||||
});
|
||||
}
|
||||
|
||||
animationFrameRef.current = requestAnimationFrame(animateFrame);
|
||||
}, [animate]);
|
||||
|
||||
const handleResize = useCallback(() => {
|
||||
const container = containerRef.current;
|
||||
const canvas = canvasRef.current;
|
||||
if (!container || !canvas) return;
|
||||
|
||||
const rect = container.getBoundingClientRect();
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
|
||||
canvas.width = rect.width * dpr;
|
||||
canvas.height = rect.height * dpr;
|
||||
canvas.style.width = `${rect.width}px`;
|
||||
canvas.style.height = `${rect.height}px`;
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (ctx) ctx.scale(dpr, dpr);
|
||||
|
||||
dimensionsRef.current = { width: rect.width, height: rect.height };
|
||||
initCircuit(rect.width, rect.height);
|
||||
}, [initCircuit]);
|
||||
|
||||
useEffect(() => {
|
||||
handleResize();
|
||||
window.addEventListener("resize", handleResize);
|
||||
animationFrameRef.current = requestAnimationFrame(animateFrame);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleResize);
|
||||
cancelAnimationFrame(animationFrameRef.current);
|
||||
};
|
||||
}, [handleResize, animateFrame]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={`absolute inset-0 pointer-events-none ${className}`}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<canvas ref={canvasRef} className="w-full h-full" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
195
apps/web/src/components/Effects/CodeSnippet.tsx
Normal file
195
apps/web/src/components/Effects/CodeSnippet.tsx
Normal file
@@ -0,0 +1,195 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { motion, useInView } from "framer-motion";
|
||||
|
||||
interface CodeSnippetProps {
|
||||
className?: string;
|
||||
variant?: "code" | "git" | "terminal";
|
||||
}
|
||||
|
||||
const codeLines = [
|
||||
{ indent: 0, text: "async deploy(config) {", color: "text-slate-500" },
|
||||
{ indent: 1, text: "const build = await compile({", color: "text-slate-400" },
|
||||
{ indent: 2, text: 'target: "production",', color: "text-slate-300" },
|
||||
{ indent: 2, text: "optimize: true,", color: "text-slate-300" },
|
||||
{ indent: 2, text: 'performance: "maximum"', color: "text-slate-300" },
|
||||
{ indent: 1, text: "});", color: "text-slate-400" },
|
||||
{ indent: 1, text: "", color: "" },
|
||||
{ indent: 1, text: "await pipeline.run([", color: "text-slate-400" },
|
||||
{ indent: 2, text: "lint, test, build, stage", color: "text-slate-300" },
|
||||
{ indent: 1, text: "]);", color: "text-slate-400" },
|
||||
{ indent: 1, text: "", color: "" },
|
||||
{ indent: 1, text: 'return { status: "live" };', color: "text-slate-400" },
|
||||
{ indent: 0, text: "}", color: "text-slate-500" },
|
||||
];
|
||||
|
||||
const gitBranches = [
|
||||
{
|
||||
type: "commit",
|
||||
branch: "main",
|
||||
label: "v2.1.0 – Production",
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
type: "branch",
|
||||
branch: "feature",
|
||||
label: "feature/redesign",
|
||||
active: true,
|
||||
},
|
||||
{ type: "commit", branch: "feature", label: "Neues Layout", active: true },
|
||||
{
|
||||
type: "commit",
|
||||
branch: "feature",
|
||||
label: "Performance-Optimierung",
|
||||
active: true,
|
||||
},
|
||||
{ type: "merge", branch: "main", label: "Merge → Production", active: false },
|
||||
{ type: "commit", branch: "main", label: "v2.2.0 – Live", active: false },
|
||||
];
|
||||
|
||||
const terminalLines = [
|
||||
{ prompt: true, text: "npm run build", delay: 0 },
|
||||
{ prompt: false, text: "✓ Compiled successfully", delay: 0.3 },
|
||||
{ prompt: false, text: "✓ Lighthouse: 98/100", delay: 0.6 },
|
||||
{ prompt: false, text: "✓ Bundle: 42kb gzipped", delay: 0.9 },
|
||||
{ prompt: true, text: "git push origin main", delay: 1.5 },
|
||||
{ prompt: false, text: "→ Deploying to production...", delay: 1.8 },
|
||||
{ prompt: false, text: "✓ Live in 12s", delay: 2.4 },
|
||||
];
|
||||
|
||||
import { CodeWindow } from "./CodeWindow";
|
||||
|
||||
export const CodeSnippet: React.FC<CodeSnippetProps> = ({
|
||||
className = "",
|
||||
variant = "code",
|
||||
}) => {
|
||||
const ref = useRef(null);
|
||||
const isInView = useInView(ref, { once: true, margin: "-10%" });
|
||||
const [visibleLineIndex, setVisibleLineIndex] = useState(-1);
|
||||
const [displayText, setDisplayText] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isInView) return;
|
||||
|
||||
const lines =
|
||||
variant === "code"
|
||||
? codeLines
|
||||
: variant === "git"
|
||||
? gitBranches.map((b) => ({ text: b.label }))
|
||||
: terminalLines;
|
||||
|
||||
const animate = async () => {
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
setVisibleLineIndex(i);
|
||||
const line = lines[i];
|
||||
const text = "text" in line ? line.text : (line as any).label || "";
|
||||
|
||||
for (let j = 0; j <= text.length; j++) {
|
||||
setDisplayText((prev) => {
|
||||
const next = [...prev];
|
||||
next[i] = text.slice(0, j);
|
||||
return next;
|
||||
});
|
||||
const speed = "prompt" in line && line.prompt ? 40 : 25;
|
||||
await new Promise((r) => setTimeout(r, speed));
|
||||
}
|
||||
|
||||
const pause = "delay" in line ? (line as any).delay * 1000 : 150;
|
||||
await new Promise((r) => setTimeout(r, pause));
|
||||
}
|
||||
};
|
||||
|
||||
animate();
|
||||
}, [isInView, variant]);
|
||||
|
||||
const title =
|
||||
variant === "code"
|
||||
? "deploy.ts"
|
||||
: variant === "git"
|
||||
? "git log"
|
||||
: "terminal";
|
||||
|
||||
return (
|
||||
<CodeWindow title={title} className={className} minHeight="380px">
|
||||
<div ref={ref}>
|
||||
{variant === "code" &&
|
||||
codeLines.map((line, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={i <= visibleLineIndex ? { opacity: 1 } : { opacity: 0 }}
|
||||
className={`${line.color} whitespace-pre flex items-center h-6`}
|
||||
style={{ paddingLeft: `${line.indent * 20}px` }}
|
||||
>
|
||||
<span>{displayText[i] || ""}</span>
|
||||
{i === visibleLineIndex && (
|
||||
<motion.span
|
||||
animate={{ opacity: [1, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 0.6 }}
|
||||
className="inline-block w-1.5 h-4 bg-slate-300 ml-1 shrink-0"
|
||||
/>
|
||||
)}
|
||||
</motion.div>
|
||||
))}
|
||||
|
||||
{variant === "git" && (
|
||||
<div className="relative">
|
||||
<div className="absolute left-[7px] top-3 bottom-3 w-px bg-slate-200" />
|
||||
{gitBranches.map((item, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, x: -5 }}
|
||||
animate={i <= visibleLineIndex ? { opacity: 1, x: 0 } : {}}
|
||||
className="flex items-center gap-4 py-1.5 relative h-8"
|
||||
>
|
||||
<div
|
||||
className={`w-4 h-4 rounded-full border-2 z-10 shrink-0 ${item.type === "merge" ? "border-slate-400 bg-slate-100" : item.active ? "border-slate-300 bg-white" : "border-slate-200 bg-slate-50"}`}
|
||||
/>
|
||||
{item.type === "branch" && (
|
||||
<span className="text-[10px] px-2 py-0.5 rounded-full bg-slate-100 border border-slate-200 text-slate-400 font-bold shrink-0">
|
||||
{item.branch}
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
className={`text-xs ${item.active ? "text-slate-500" : "text-slate-300"} truncate`}
|
||||
>
|
||||
{displayText[i] || ""}
|
||||
</span>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{variant === "terminal" &&
|
||||
terminalLines.map((line, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={i <= visibleLineIndex ? { opacity: 1 } : {}}
|
||||
className="flex items-start gap-2 py-0.5 min-h-[1.5rem]"
|
||||
>
|
||||
{line.prompt && (
|
||||
<span className="text-slate-300 select-none shrink-0">❯</span>
|
||||
)}
|
||||
<span
|
||||
className={
|
||||
line.prompt ? "text-slate-500 font-medium" : "text-slate-300"
|
||||
}
|
||||
>
|
||||
{displayText[i] || ""}
|
||||
</span>
|
||||
{i === visibleLineIndex && (
|
||||
<motion.span
|
||||
animate={{ opacity: [1, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 0.6 }}
|
||||
className="inline-block w-1.5 h-4 bg-slate-300 ml-0.5 shrink-0"
|
||||
/>
|
||||
)}
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</CodeWindow>
|
||||
);
|
||||
};
|
||||
59
apps/web/src/components/Effects/CodeWindow.tsx
Normal file
59
apps/web/src/components/Effects/CodeWindow.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { cn } from "../../utils/cn";
|
||||
|
||||
interface CodeWindowProps {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
actions?: React.ReactNode;
|
||||
fixedHeight?: boolean;
|
||||
minHeight?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* CodeWindow: A shared, stable browser-frame chassis for code, terminal, and diagrams.
|
||||
* - Enforces dimension stability to prevent layout shifts.
|
||||
* - Standardizes the "Systems, not Brochures" aesthetic.
|
||||
*/
|
||||
export const CodeWindow: React.FC<CodeWindowProps> = ({
|
||||
title,
|
||||
children,
|
||||
className = "",
|
||||
actions,
|
||||
fixedHeight = false,
|
||||
minHeight = "380px",
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative rounded-xl border border-slate-100 bg-slate-50/50 backdrop-blur-sm overflow-hidden w-full max-w-[600px] mx-auto flex-shrink-0 flex flex-col",
|
||||
fixedHeight && "h-[400px]",
|
||||
className,
|
||||
)}
|
||||
style={!fixedHeight && minHeight ? { minHeight } : {}}
|
||||
>
|
||||
{/* Window chrome */}
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-slate-100 bg-white/50 shrink-0">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="w-2 h-2 rounded-full bg-slate-200" />
|
||||
<div className="w-2 h-2 rounded-full bg-slate-200" />
|
||||
<div className="w-2 h-2 rounded-full bg-slate-200" />
|
||||
<span className="ml-3 text-[9px] font-mono text-slate-300 uppercase tracking-widest select-none">
|
||||
{title}
|
||||
</span>
|
||||
</div>
|
||||
{actions && <div className="flex items-center gap-2">{actions}</div>}
|
||||
</div>
|
||||
|
||||
{/* Content area */}
|
||||
<div className="flex-1 overflow-x-auto overflow-y-auto p-4 md:p-6 font-mono text-xs md:text-sm leading-relaxed relative">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{/* Bottom gradient fade for aesthetics/depth */}
|
||||
<div className="absolute bottom-0 left-0 right-0 h-8 bg-gradient-to-t from-slate-50/80 to-transparent pointer-events-none" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
120
apps/web/src/components/Effects/DataFlow.tsx
Normal file
120
apps/web/src/components/Effects/DataFlow.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
interface DataFlowProps {
|
||||
className?: string;
|
||||
lines?: number;
|
||||
speed?: "slow" | "normal" | "fast";
|
||||
}
|
||||
|
||||
export const DataFlow: React.FC<DataFlowProps> = ({
|
||||
className = "",
|
||||
lines = 3,
|
||||
speed = "normal",
|
||||
}) => {
|
||||
const speedMap = { slow: "8s", normal: "5s", fast: "3s" };
|
||||
const duration = speedMap[speed];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`relative w-full overflow-hidden ${className}`}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 1200 40"
|
||||
className="w-full h-8 md:h-10"
|
||||
preserveAspectRatio="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
{Array.from({ length: lines }).map((_, i) => {
|
||||
const y = 8 + (i * 24) / lines;
|
||||
const delay = i * 0.8;
|
||||
return (
|
||||
<g key={i}>
|
||||
{/* Static trace line */}
|
||||
<line
|
||||
x1="0"
|
||||
y1={y}
|
||||
x2="1200"
|
||||
y2={y}
|
||||
stroke="rgba(226, 232, 240, 0.3)"
|
||||
strokeWidth="0.5"
|
||||
/>
|
||||
{/* Animated data packet */}
|
||||
<circle r="2" fill="rgba(148, 163, 184, 0.6)">
|
||||
<animateMotion
|
||||
dur={duration}
|
||||
repeatCount="indefinite"
|
||||
begin={`${delay}s`}
|
||||
path={`M-20,${y} L1220,${y}`}
|
||||
/>
|
||||
</circle>
|
||||
{/* Trailing glow */}
|
||||
<circle r="6" fill="rgba(148, 163, 184, 0.08)">
|
||||
<animateMotion
|
||||
dur={duration}
|
||||
repeatCount="indefinite"
|
||||
begin={`${delay}s`}
|
||||
path={`M-20,${y} L1220,${y}`}
|
||||
/>
|
||||
</circle>
|
||||
{/* Secondary packet (opposite direction, slower) */}
|
||||
<rect
|
||||
width="12"
|
||||
height="1"
|
||||
rx="0.5"
|
||||
fill="rgba(203, 213, 225, 0.3)"
|
||||
>
|
||||
<animateMotion
|
||||
dur={`${parseFloat(duration) * 1.4}s`}
|
||||
repeatCount="indefinite"
|
||||
begin={`${delay + 2}s`}
|
||||
path={`M1220,${y + 2} L-20,${y + 2}`}
|
||||
/>
|
||||
</rect>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Junction nodes */}
|
||||
{[200, 500, 800, 1050].map((x, i) => (
|
||||
<g key={`node-${i}`}>
|
||||
<circle cx={x} cy="20" r="2" fill="rgba(203, 213, 225, 0.4)">
|
||||
<animate
|
||||
attributeName="r"
|
||||
values="1.5;2.5;1.5"
|
||||
dur="3s"
|
||||
begin={`${i * 0.7}s`}
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</circle>
|
||||
<circle
|
||||
cx={x}
|
||||
cy="20"
|
||||
r="6"
|
||||
fill="none"
|
||||
stroke="rgba(203, 213, 225, 0.15)"
|
||||
strokeWidth="0.5"
|
||||
>
|
||||
<animate
|
||||
attributeName="r"
|
||||
values="4;8;4"
|
||||
dur="3s"
|
||||
begin={`${i * 0.7}s`}
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
values="0.3;0;0.3"
|
||||
dur="3s"
|
||||
begin={`${i * 0.7}s`}
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</circle>
|
||||
</g>
|
||||
))}
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
82
apps/web/src/components/Effects/GradientMesh.tsx
Normal file
82
apps/web/src/components/Effects/GradientMesh.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
interface GradientMeshProps {
|
||||
className?: string;
|
||||
variant?: "subtle" | "metallic" | "warm";
|
||||
animate?: boolean;
|
||||
}
|
||||
|
||||
export const GradientMesh: React.FC<GradientMeshProps> = ({
|
||||
className = "",
|
||||
variant = "subtle",
|
||||
animate = true,
|
||||
}) => {
|
||||
const gradients = {
|
||||
subtle: {
|
||||
bg: "transparent",
|
||||
blob1: "rgba(226, 232, 240, 0.6)",
|
||||
blob2: "rgba(241, 245, 249, 0.7)",
|
||||
blob3: "rgba(203, 213, 225, 0.35)",
|
||||
},
|
||||
metallic: {
|
||||
bg: "transparent",
|
||||
blob1: "rgba(186, 206, 235, 0.4)",
|
||||
blob2: "rgba(214, 224, 240, 0.5)",
|
||||
blob3: "rgba(170, 190, 220, 0.25)",
|
||||
},
|
||||
warm: {
|
||||
bg: "transparent",
|
||||
blob1: "rgba(241, 245, 249, 0.6)",
|
||||
blob2: "rgba(248, 250, 252, 0.7)",
|
||||
blob3: "rgba(226, 232, 240, 0.4)",
|
||||
},
|
||||
};
|
||||
|
||||
const colors = gradients[variant];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`absolute inset-0 pointer-events-none overflow-hidden ${className}`}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{/* Large Blob 1 */}
|
||||
<div
|
||||
className={`absolute rounded-full ${animate ? "animate-gradient-blob-1" : ""}`}
|
||||
style={{
|
||||
width: "900px",
|
||||
height: "900px",
|
||||
background: `radial-gradient(circle, ${colors.blob1} 0%, transparent 65%)`,
|
||||
top: "-20%",
|
||||
left: "-10%",
|
||||
filter: "blur(80px)",
|
||||
}}
|
||||
/>
|
||||
{/* Large Blob 2 */}
|
||||
<div
|
||||
className={`absolute rounded-full ${animate ? "animate-gradient-blob-2" : ""}`}
|
||||
style={{
|
||||
width: "800px",
|
||||
height: "800px",
|
||||
background: `radial-gradient(circle, ${colors.blob2} 0%, transparent 65%)`,
|
||||
bottom: "-25%",
|
||||
right: "-10%",
|
||||
filter: "blur(70px)",
|
||||
}}
|
||||
/>
|
||||
{/* Accent Blob 3 */}
|
||||
<div
|
||||
className={`absolute rounded-full ${animate ? "animate-gradient-blob-3" : ""}`}
|
||||
style={{
|
||||
width: "600px",
|
||||
height: "600px",
|
||||
background: `radial-gradient(circle, ${colors.blob3} 0%, transparent 65%)`,
|
||||
top: "30%",
|
||||
left: "25%",
|
||||
filter: "blur(60px)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
6
apps/web/src/components/Effects/index.ts
Normal file
6
apps/web/src/components/Effects/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export { CircuitBoard } from "./CircuitBoard";
|
||||
export { DataFlow } from "./DataFlow";
|
||||
export { BinaryStream } from "./BinaryStream";
|
||||
export { GradientMesh } from "./GradientMesh";
|
||||
export { CodeSnippet } from "./CodeSnippet";
|
||||
export { AbstractCircuit } from "./AbstractCircuit";
|
||||
Reference in New Issue
Block a user