wip: foto da pasta principal antes de trocar de branch
CI / verify (push) Failing after 5m29s

Estado do disco em 14/10/2026, sem filtrar. Inclui as copias .sync-conflict-* do Syncthing de 07/10 e os 15 arquivos que o Syncthing apagou; todo esse conteudo ja existia em outros commits. So havia fora do Git: AGENTS.md, CLAUDE.md e a alteracao em data/project-health.json.
This commit is contained in:
Evandro Fonseca Junior committed 2026-10-14 00:42:49 -03:00
1 parent b2f3899dc2
commit dfcadc71fa
77 files changed
+6944 -2419

No files matched your search

@@ -1,13 +1,103 @@
import { render } from "@testing-library/react";
import { fireEvent, render, screen } from "@testing-library/react";
import FilmGrain from "./FilmGrain";
import ScrollVelocityText from "./ScrollVelocityText";
import SectionDivider from "./SectionDivider";
function mockReducedMotion(matches: boolean) {
Object.defineProperty(window, "matchMedia", {
writable: true,
value: jest.fn().mockImplementation((query: string) => ({
matches: query.includes("prefers-reduced-motion") ? matches : false,
media: query,
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
}
describe("static cinematic effects", () => {
it("renders grain without a canvas", () => {
beforeEach(() => mockReducedMotion(false));
it("renders grain and every divider without a canvas", () => {
const grain = render(<FilmGrain />);
expect(grain.container.firstElementChild).toHaveAttribute(
"aria-hidden",
"true",
);
expect(grain.container.querySelector("canvas")).not.toBeInTheDocument();
grain.unmount();
for (const variant of ["gradient", "pulse", "waveform"] as const) {
const divider = render(<SectionDivider variant={variant} />);
expect(divider.container.firstElementChild).toHaveAttribute(
"aria-hidden",
"true",
);
expect(divider.container.querySelector("canvas")).not.toBeInTheDocument();
expect(divider.container.firstElementChild).not.toHaveAttribute(
"tabindex",
);
divider.unmount();
}
});
it("exposes a persistent pause control for an automatic marquee", () => {
render(<ScrollVelocityText text="SOFTWARE ENGINEERING" direction="left" />);
const pause = screen.getByRole("button", { name: "Pausar animação" });
const track = document.getElementById(
pause.getAttribute("aria-controls") ?? "",
);
expect(pause).toHaveAttribute("aria-pressed", "false");
expect(track).not.toHaveStyle({ animationPlayState: "paused" });
expect(
screen.getByText("SOFTWARE ENGINEERING", {
selector: '[aria-hidden="false"]',
}),
).toBeVisible();
expect(screen.getAllByText("SOFTWARE ENGINEERING")).toHaveLength(2);
expect(
screen
.getAllByText("SOFTWARE ENGINEERING")
.slice(1)
.every((copy) => copy.getAttribute("aria-hidden") === "true"),
).toBe(true);
fireEvent.click(pause);
const resume = screen.getByRole("button", { name: "Retomar animação" });
expect(resume).toHaveAttribute("aria-pressed", "true");
expect(track).toHaveStyle({ animationPlayState: "paused" });
fireEvent.click(resume);
expect(
screen.getByRole("button", { name: "Pausar animação" }),
).toHaveAttribute("aria-pressed", "false");
expect(track).not.toHaveStyle({ animationPlayState: "paused" });
});
it("starts static under reduced motion without a redundant control", () => {
mockReducedMotion(true);
render(<ScrollVelocityText text="VAMOS CONVERSAR" direction="right" />);
expect(
screen.queryByRole("button", { name: /animação/i }),
).not.toBeInTheDocument();
const copies = screen.getAllByText("VAMOS CONVERSAR");
expect(copies).toHaveLength(2);
expect(copies[0]).toBeVisible();
expect(copies[0]).toHaveAttribute("aria-hidden", "false");
expect(copies[0].closest("[style]")).toHaveStyle({
animationPlayState: "paused",
});
expect(
copies
.slice(1)
.every((copy) => copy.getAttribute("aria-hidden") === "true"),
).toBe(true);
});
});
+18 -56
View File
@@ -1,15 +1,12 @@
import { act, fireEvent, render, screen } from "@testing-library/react";
import {
resetHomeArrivalState,
useHomeFragmentNavigation,
} from "./useHomeFragmentNavigation";
import { useHomeFragmentNavigation } from "./useHomeFragmentNavigation";
function HomeNavigation() {
const onLayoutReady = useHomeFragmentNavigation();
return (
<>
<button type="button" onClick={onLayoutReady}>
Establish layout
Establish notebook layout
</button>
<a href="/#about">Sobre</a>
<a href="/#contact">Contato</a>
@@ -26,13 +23,11 @@ function HomeNavigation() {
const originalScrollIntoView = HTMLElement.prototype.scrollIntoView;
beforeEach(() => {
resetHomeArrivalState();
window.history.replaceState(null, "", "/");
HTMLElement.prototype.scrollIntoView = jest.fn();
});
afterEach(() => {
jest.restoreAllMocks();
window.history.replaceState(null, "", "/");
if (originalScrollIntoView) {
HTMLElement.prototype.scrollIntoView = originalScrollIntoView;
@@ -42,59 +37,20 @@ afterEach(() => {
});
describe("home fragment navigation", () => {
it("starts an unfragmented visit at the top after the home establishes layout", () => {
let restoredPosition = 3200;
jest
.spyOn(window, "scrollTo")
.mockImplementation((options: ScrollToOptions | number) => {
if (typeof options === "object")
restoredPosition = options.top ?? restoredPosition;
});
render(<HomeNavigation />);
fireEvent.click(screen.getByRole("button", { name: "Establish layout" }));
expect(restoredPosition).toBe(0);
});
it("leaves a same-document return to the position the browser restores", () => {
const scrollTo = jest
.spyOn(window, "scrollTo")
.mockImplementation(() => {});
const firstVisit = render(<HomeNavigation />);
fireEvent.click(screen.getByRole("button", { name: "Establish layout" }));
firstVisit.unmount();
expect(window.history.scrollRestoration).not.toBe("manual");
scrollTo.mockClear();
render(<HomeNavigation />);
fireEvent.click(screen.getByRole("button", { name: "Establish layout" }));
expect(scrollTo).not.toHaveBeenCalled();
expect(window.history.scrollRestoration).not.toBe("manual");
});
it("does not reset the visitor's position after intentional scrolling during startup", () => {
let position = 0;
jest
.spyOn(window, "scrollTo")
.mockImplementation((options: ScrollToOptions | number) => {
if (typeof options === "object") position = options.top ?? position;
});
render(<HomeNavigation />);
position = 600;
fireEvent.wheel(window, { deltaY: 600 });
fireEvent.click(screen.getByRole("button", { name: "Establish layout" }));
expect(position).toBe(600);
});
it("waits for the home layout on fragment arrival, then never steals focus on refresh", () => {
it("waits for the notebook layout on fragment arrival, then never steals focus on refresh", () => {
window.history.replaceState(null, "", "/#contact");
render(<HomeNavigation />);
const contact = screen.getByRole("region", { name: "Contato" });
expect(contact).not.toHaveFocus();
fireEvent.click(screen.getByRole("button", { name: "Establish layout" }));
fireEvent.click(
screen.getByRole("button", { name: "Establish notebook layout" }),
);
expect(contact).toHaveFocus();
screen.getByRole("button", { name: "Read biography" }).focus();
fireEvent.click(screen.getByRole("button", { name: "Establish layout" }));
fireEvent.click(
screen.getByRole("button", { name: "Establish notebook layout" }),
);
expect(
screen.getByRole("button", { name: "Read biography" }),
).toHaveFocus();
@@ -106,14 +62,18 @@ describe("home fragment navigation", () => {
const message = screen.getByRole("button", { name: "Send message" });
message.focus();
fireEvent.wheel(window, { deltaY: 200 });
fireEvent.click(screen.getByRole("button", { name: "Establish layout" }));
fireEvent.click(
screen.getByRole("button", { name: "Establish notebook layout" }),
);
expect(message).toHaveFocus();
});
it("keeps repeated same-fragment links native while moving focus to the destination", () => {
window.history.replaceState(null, "", "/#about");
render(<HomeNavigation />);
fireEvent.click(screen.getByRole("button", { name: "Establish layout" }));
fireEvent.click(
screen.getByRole("button", { name: "Establish notebook layout" }),
);
const link = screen.getByRole("link", { name: "Sobre" });
link.focus();
expect(fireEvent.click(link)).toBe(true);
@@ -135,7 +95,9 @@ describe("home fragment navigation", () => {
window.history.replaceState(null, "", "/#contact");
window.dispatchEvent(new HashChangeEvent("hashchange"));
});
fireEvent.click(screen.getByRole("button", { name: "Establish layout" }));
fireEvent.click(
screen.getByRole("button", { name: "Establish notebook layout" }),
);
expect(message).toHaveFocus();
});
});
+36 -27
View File
@@ -1,6 +1,5 @@
"use client";
import { useEffect, useRef } from "react";
import type {
PortfolioExperience,
PortfolioProjectView,
@@ -8,63 +7,73 @@ import type {
} from "@/lib/portfolio-data";
import About from "../About";
import Contact from "../Contact";
import EngineeringWithAI from "../EngineeringWithAI";
import Experience from "../Experience";
import Footer from "../Footer";
import Hero, { type HeroStats } from "../Hero";
import Hero from "../Hero";
import LighthouseMetrics from "../LighthouseMetrics";
import Projects from "../Projects";
import Skills from "../Skills";
import NotebookStory from "../story/NotebookStory";
import { WorkModalities } from "../WorkModalities";
import ExperienceDisclosure from "./ExperienceDisclosure";
import { useCinematicScroll } from "./useCinematicScroll";
import ScrollVelocityText from "./ScrollVelocityText";
import SectionDivider from "./SectionDivider";
import { useHomeFragmentNavigation } from "./useHomeFragmentNavigation";
export default function CinematicHome({
projects,
experiences,
skillCategories,
heroStats,
}: {
heroStats?: HeroStats;
projects: PortfolioProjectView[];
experiences: PortfolioExperience[];
skillCategories: PortfolioSkillCategory[];
}) {
const onLayoutReady = useHomeFragmentNavigation();
const mainRef = useRef<HTMLElement>(null);
useCinematicScroll(mainRef);
// o layout da home é estável na montagem: a chegada por fragmento (#contato etc.) já pode ser resolvida
useEffect(() => onLayoutReady(), [onLayoutReady]);
return (
<div className="flex min-h-screen flex-col overflow-x-clip">
{/* Cada filho direto de <main> é uma cena; useCinematicScroll liga as passagens entre elas. */}
<main ref={mainRef} className="ds-main relative z-0">
<Hero stats={heroStats} />
<main className="ds-main">
<Hero />
{/* Marquee divider after hero */}
<div className="overflow-hidden bg-[var(--ink)] py-6 text-[rgba(244,242,237,0.06)]">
<ScrollVelocityText text="SOFTWARE ENGINEERING" direction="left" />
</div>
<NotebookStory projects={projects} onLayoutReady={onLayoutReady} />
<About />
<SectionDivider variant="waveform" />
<Skills categories={skillCategories} />
<div className="overflow-hidden bg-[var(--ink)] py-4 text-[rgba(204,255,0,0.04)]">
<ScrollVelocityText
text="NEXT.JS TAURI RUST NODE.JS TYPESCRIPT"
direction="right"
/>
</div>
<Experience experiences={experiences} />
<SectionDivider variant="gradient" />
<Projects projects={projects} />
<WorkModalities />
<EngineeringWithAI />
<LighthouseMetrics />
<div className="bg-[var(--paper)]">
<About />
<SectionDivider variant="pulse" />
<ExperienceDisclosure>
<Skills categories={skillCategories} />
<Experience experiences={experiences} />
</ExperienceDisclosure>
<WorkModalities />
<div className="overflow-hidden bg-[var(--ink)] py-6 text-[rgba(244,242,237,0.05)]">
<ScrollVelocityText text="VAMOS CONVERSAR" direction="left" />
</div>
<Contact />
</main>
<div className="relative z-10">
<Footer />
</div>
<Footer />
</div>
);
}
@@ -0,0 +1,146 @@
import { act, render, waitFor } from "@testing-library/react";
import createGlobe from "cobe";
import { ControlRoomGlobe } from "./ControlRoomGlobe";
const mockDestroy = jest.fn();
jest.mock("cobe", () => ({
__esModule: true,
default: jest.fn(() => ({ destroy: mockDestroy })),
}));
jest.mock("motion/react", () => ({
useMotionValue: (initial: number) => ({
get: () => initial,
set: jest.fn(),
}),
useSpring: (value: unknown) => value,
}));
let intersectionCallback: IntersectionObserverCallback | undefined;
let resizeCallback: ResizeObserverCallback | undefined;
let measuredWidth = 400;
function setDocumentHidden(hidden: boolean) {
Object.defineProperty(document, "hidden", {
configurable: true,
value: hidden,
});
}
function setGlobeIntersects(isIntersecting: boolean) {
const entry = { isIntersecting } as IntersectionObserverEntry;
act(() => intersectionCallback?.([entry], {} as IntersectionObserver));
}
function renderCallback() {
expect(createGlobe).toHaveBeenCalledTimes(1);
const call = jest.mocked(createGlobe).mock.calls[0];
if (!call) throw new Error("Globe was not initialized");
return call[1].onRender;
}
describe("ControlRoomGlobe lifecycle", () => {
beforeEach(() => {
jest.clearAllMocks();
setDocumentHidden(false);
intersectionCallback = undefined;
resizeCallback = undefined;
measuredWidth = 400;
jest
.spyOn(HTMLElement.prototype, "offsetWidth", "get")
.mockImplementation(() => measuredWidth);
jest
.spyOn(HTMLCanvasElement.prototype, "getContext")
.mockReturnValue({} as unknown as GPUCanvasContext);
class MockIntersectionObserver {
readonly root = null;
readonly rootMargin = "";
readonly thresholds = [];
constructor(callback: IntersectionObserverCallback) {
intersectionCallback = callback;
}
disconnect = jest.fn();
observe = jest.fn();
takeRecords = () => [];
unobserve = jest.fn();
}
global.IntersectionObserver =
MockIntersectionObserver as unknown as typeof IntersectionObserver;
class MockResizeObserver {
constructor(callback: ResizeObserverCallback) {
resizeCallback = callback;
}
disconnect = jest.fn();
observe = jest.fn();
unobserve = jest.fn();
}
global.ResizeObserver =
MockResizeObserver as unknown as typeof ResizeObserver;
});
afterEach(() => {
jest.restoreAllMocks();
});
it("pauses for a hidden tab without losing the intersection flag", async () => {
render(<ControlRoomGlobe />);
await waitFor(() => expect(createGlobe).toHaveBeenCalledTimes(1));
const renderFrame = renderCallback();
const state = { phi: 0, theta: 0, width: 0, height: 0 };
setGlobeIntersects(true);
renderFrame(state);
expect(state.phi).not.toBe(0);
setDocumentHidden(true);
const pausedPhi = state.phi;
renderFrame(state);
expect(state.phi).toBe(pausedPhi);
setDocumentHidden(false);
renderFrame(state);
expect(state.phi).not.toBe(pausedPhi);
setGlobeIntersects(false);
const offscreenPhi = state.phi;
setDocumentHidden(true);
setDocumentHidden(false);
renderFrame(state);
expect(state.phi).toBe(offscreenPhi);
});
it("keeps one Cobe instance while ResizeObserver updates render dimensions", async () => {
render(<ControlRoomGlobe />);
await waitFor(() => expect(createGlobe).toHaveBeenCalledTimes(1));
measuredWidth = 520;
const entry = {
contentRect: { width: measuredWidth },
} as ResizeObserverEntry;
act(() => resizeCallback?.([entry], {} as ResizeObserver));
setGlobeIntersects(true);
const state = { phi: 0, theta: 0, width: 0, height: 0 };
renderCallback()(state);
expect(createGlobe).toHaveBeenCalledTimes(1);
expect(state.width).toBe(1040);
expect(state.height).toBe(1040);
});
it("keeps the reduced-motion globe static while tracking dimensions", async () => {
render(<ControlRoomGlobe autoRotate={false} />);
await waitFor(() => expect(createGlobe).toHaveBeenCalled());
setGlobeIntersects(true);
const state = { phi: 0, theta: 0, width: 0, height: 0 };
renderCallback()(state);
expect(state).toEqual({ phi: 0, theta: 0, width: 800, height: 800 });
});
});
@@ -0,0 +1,210 @@
"use client";
import createGlobe, { type COBEOptions } from "cobe";
import { useMotionValue, useSpring } from "motion/react";
import { useEffect, useRef } from "react";
import { cn } from "@/lib/utils";
const MOVEMENT_DAMPING = 1400;
const JOINVILLE_LAT = -26.3044;
const JOINVILLE_LNG = -48.8456;
// cobe espera radianos: theta = latitude da banda de frente (Trópico de Capricórnio), phi = longitude de frente (Joinville)
const TROPIC_OF_CAPRICORN_RAD = (-23.4364 * Math.PI) / 180;
const JOINVILLE_LNG_RAD = (JOINVILLE_LNG * Math.PI) / 180;
const CONTROL_ROOM_GLOBE_CONFIG: COBEOptions = {
width: 800,
height: 800,
onRender: () => {},
devicePixelRatio: 2,
phi: JOINVILLE_LNG_RAD,
theta: TROPIC_OF_CAPRICORN_RAD,
dark: 0,
diffuse: 1.2,
mapSamples: 16000,
mapBrightness: 6,
baseColor: [0.32, 0.32, 0.32],
markerColor: [204 / 255, 255 / 255, 0 / 255],
glowColor: [204 / 255, 255 / 255, 0 / 255],
markers: [
{ location: [JOINVILLE_LAT, JOINVILLE_LNG], size: 0.12 },
{ location: [-25.4284, -49.2733], size: 0.06 },
{ location: [-26.0167, -48.5], size: 0.05 },
{ location: [-25.9333, -48.7833], size: 0.04 },
],
} as const satisfies COBEOptions;
export function ControlRoomGlobe({
className,
config = CONTROL_ROOM_GLOBE_CONFIG,
autoRotate = true,
rotationSpeed = 0.003,
}: {
className?: string;
config?: COBEOptions;
autoRotate?: boolean;
rotationSpeed?: number;
}) {
const widthRef = useRef(0);
const phiRef = useRef(config.phi ?? JOINVILLE_LNG_RAD);
// Rotação livre: oscilação de theta (inclinação multi-eixo) + arraste vertical
const thetaPhaseRef = useRef(0);
const thetaDragRef = useRef(0);
const canvasRef = useRef<HTMLCanvasElement>(null);
const pointerInteracting = useRef<number | null>(null);
const pointerYOrigin = useRef<number | null>(null);
const intersectsRef = useRef(false);
const r = useMotionValue(0);
const rs = useSpring(r, {
mass: 1,
damping: 30,
stiffness: 100,
});
const updatePointerInteraction = (value: number | null) => {
pointerInteracting.current = value;
if (canvasRef.current) {
canvasRef.current.style.cursor = value !== null ? "grabbing" : "grab";
}
};
const updateMovement = (clientX: number, clientY?: number) => {
if (pointerInteracting.current !== null) {
const delta = clientX - pointerInteracting.current;
r.set(r.get() + delta / MOVEMENT_DAMPING);
// Eixo vertical: arrasto inclina o globo (limitado p/ não virar de cabeça pra baixo)
if (clientY !== undefined && pointerYOrigin.current !== null) {
const deltaY = clientY - pointerYOrigin.current;
thetaDragRef.current = Math.max(
-0.9,
Math.min(0.9, thetaDragRef.current + deltaY / MOVEMENT_DAMPING),
);
pointerYOrigin.current = clientY;
}
}
};
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return undefined;
const dpr = config.devicePixelRatio;
const updateWidth = (entry?: ResizeObserverEntry) => {
const nextWidth =
entry?.contentRect.width || canvas.offsetWidth || config.width / dpr;
if (nextWidth > 0) widthRef.current = nextWidth;
};
updateWidth();
const resizeObserver = new ResizeObserver(([entry]) => updateWidth(entry));
resizeObserver.observe(canvas);
const intersectionObserver = new IntersectionObserver(
([entry]) => {
intersectsRef.current = entry.isIntersecting;
},
{ rootMargin: "100px" },
);
intersectionObserver.observe(canvas);
// Falha suave: sem WebGL (ou canvas já vinculado a outro tipo de contexto) o phenomenon
// lança "reading 'enable' of null" e derrubaria a árvore React inteira — card ficaria preto.
let globe: { destroy(): void } | null = null;
try {
const supportsWebGL =
canvas.getContext("webgl2") ?? canvas.getContext("webgl");
if (supportsWebGL) {
const initialRenderWidth = widthRef.current * dpr;
globe = createGlobe(canvas, {
...config,
width: initialRenderWidth,
height: initialRenderWidth,
onRender: (state) => {
const renderWidth = widthRef.current * dpr;
state.width = renderWidth;
state.height = renderWidth;
if (!autoRotate || !intersectsRef.current || document.hidden)
return;
if (!pointerInteracting.current) {
phiRef.current += rotationSpeed;
thetaPhaseRef.current += rotationSpeed * 0.9;
}
state.phi = phiRef.current + rs.get();
// Rotação livre em todos os eixos: spin (phi) + guinada senoidal (theta) + inclinação por arraste vertical
state.theta =
(config.theta ?? TROPIC_OF_CAPRICORN_RAD) +
Math.sin(thetaPhaseRef.current) * 0.35 +
thetaDragRef.current;
},
});
}
} catch {
globe = null;
}
if (!globe) {
// Sem globo: fallback visível (gradiente do card permanece), página viva.
canvas.style.opacity = "1";
return () => {
resizeObserver.disconnect();
intersectionObserver.disconnect();
};
}
const revealTimer = window.setTimeout(() => {
if (canvasRef.current) {
canvasRef.current.style.opacity = "1";
}
}, 0);
return () => {
window.clearTimeout(revealTimer);
globe.destroy();
resizeObserver.disconnect();
intersectionObserver.disconnect();
};
}, [rs, config, autoRotate, rotationSpeed]);
return (
<div
className={cn(
"relative mx-auto aspect-[1/1] w-full max-w-[520px]",
className,
)}
aria-hidden="true"
>
<canvas
className={cn(
"size-full opacity-0 transition-opacity duration-700 [contain:layout_paint_size]",
)}
ref={canvasRef}
onPointerDown={(e) => {
pointerInteracting.current = e.clientX;
pointerYOrigin.current = e.clientY;
updatePointerInteraction(e.clientX);
}}
onPointerUp={() => {
updatePointerInteraction(null);
pointerYOrigin.current = null;
}}
onPointerOut={() => updatePointerInteraction(null)}
onPointerLeave={() => {
updatePointerInteraction(null);
pointerYOrigin.current = null;
}}
onMouseMove={(e) => updateMovement(e.clientX, e.clientY)}
onTouchMove={(e) =>
e.touches[0] &&
updateMovement(e.touches[0].clientX, e.touches[0].clientY)
}
/>
<span className="sr-only">
Globo interativo centrado em Joinville, Santa Catarina, com marcadores
indicando presença operacional na região Norte de SC e Curitiba.
</span>
</div>
);
}
+404
View File
@@ -0,0 +1,404 @@
"use client";
import { useEffect, useRef } from "react";
const MAX_DPR = 1.5;
const ASSEMBLE_MS = 2000;
const REPEL_RADIUS_PX = 80;
// Densidade: ~1 star por STAR_AREA_DIVISOR px² CSS, entre piso e teto absolutos
const STAR_AREA_DIVISOR = 700;
const MIN_STARS = 300;
const MAX_STARS = 850;
const OFFSCREEN_POINTER = -100000;
type Particle = {
sx: number; // origem da assemblei (espalhada) no primeiro load
sy: number;
tx: number; // âncora na grade com jitter
ty: number;
ox: number; // deslocamento acumulado do repel do ponteiro
oy: number;
delay: number;
fillStyle: string;
baseAlpha: number;
size: number;
phase: number;
speed: number;
amp: number;
ttl: number;
birth: number;
};
type Rgb = [number, number, number];
const FALLBACK_SLASH: Rgb = [204, 255, 0];
const FALLBACK_PAPER: Rgb = [244, 242, 237];
function hexToRgb(hex: string): Rgb | null {
const value = hex.trim().replace("#", "");
const full =
value.length === 3
? value
.split("")
.map((char) => char + char)
.join("")
: value;
if (!/^[0-9a-fA-F]{6}$/.test(full)) return null;
return [
Number.parseInt(full.slice(0, 2), 16),
Number.parseInt(full.slice(2, 4), 16),
Number.parseInt(full.slice(4, 6), 16),
];
}
function easeOutQuint(progress: number) {
return 1 - (1 - progress) ** 5;
}
export function HeroStarfield() {
const containerRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const container = containerRef.current;
const canvas = canvasRef.current;
if (!container || !canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const reducedMotion = window.matchMedia(
"(prefers-reduced-motion: reduce)",
).matches;
let rafId = 0;
let running = false;
let cancelled = false;
let resampleTimer = 0;
let sampleRetryTimer = 0;
let sampleAttempts = 0;
let particles: Particle[] = [];
let hasAssembled = false;
let elapsed = 0;
let lastTime = 0;
let dpr = 1;
let intersecting = true;
const pointer = { x: OFFSCREEN_POINTER, y: OFFSCREEN_POINTER };
let rect = container.getBoundingClientRect();
const rootStyles = getComputedStyle(document.documentElement);
const slashRgb =
hexToRgb(rootStyles.getPropertyValue("--slash")) ?? FALLBACK_SLASH;
const paperRgb =
hexToRgb(rootStyles.getPropertyValue("--paper")) ?? FALLBACK_PAPER;
// Grade estratificada: uma star por célula com jitter interno → cobertura
// uniforme do card, sem os aglomerados/vazios do random puro
const grid = {
cols: 1,
rows: 1,
cellW: canvas.width || 1,
cellH: canvas.height || 1,
};
const jitteredCellPosition = (cell: number): [number, number] => {
const cx = cell % grid.cols;
const cy = Math.floor(cell / grid.cols);
return [
(cx + Math.random()) * grid.cellW,
(cy + Math.random()) * grid.cellH,
];
};
const makeParticle = (tx: number, ty: number): Particle => {
const useSlash = Math.random() < 0.45;
const rgb = useSlash ? slashRgb : paperRgb;
return {
tx,
ty,
sx: hasAssembled ? tx : (Math.random() * 1.3 - 0.15) * canvas.width,
sy: hasAssembled ? ty : (Math.random() * 1.3 - 0.15) * canvas.height,
ox: 0,
oy: 0,
delay: Math.random() * 0.35,
fillStyle: `rgb(${rgb[0]},${rgb[1]},${rgb[2]})`,
baseAlpha: 0.5 + Math.random() * 0.45,
size: (Math.random() < 0.72 ? 2 : 3) * dpr,
phase: Math.random() * Math.PI * 2,
// deriva lenta: período ~2–5s com amplitude 2,5–5,5px CSS → "levemente"
speed: 0.0011 + Math.random() * 0.0016,
amp: (2.5 + Math.random() * 3) * dpr,
ttl: 4000 + Math.random() * 6000,
// birth negativo: estagia os ciclos de vida para o spawn/death já rodar disperso no load
birth: -Math.random() * 10000,
};
};
const sample = () => {
const width = container.clientWidth;
const height = container.clientHeight;
if (width < 60 || height < 60) return;
dpr = Math.min(window.devicePixelRatio || 1, MAX_DPR);
canvas.width = Math.round(width * dpr);
canvas.height = Math.round(height * dpr);
rect = container.getBoundingClientRect();
const count = Math.round(
Math.min(
MAX_STARS,
Math.max(MIN_STARS, (width * height) / STAR_AREA_DIVISOR),
),
);
// células quadradas-ish para a proporção do card
grid.cols = Math.max(
1,
Math.round(Math.sqrt(count * (canvas.width / canvas.height))),
);
grid.rows = Math.max(1, Math.ceil(count / grid.cols));
grid.cellW = canvas.width / grid.cols;
grid.cellH = canvas.height / grid.rows;
// ordem embaralhada das células: cada star ocupa uma célula distinta no spawn
const cells = Array.from({ length: grid.cols * grid.rows }, (_, i) => i);
for (let i = cells.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
const tmp = cells[i];
cells[i] = cells[j];
cells[j] = tmp;
}
particles = cells.slice(0, count).map((cell) => {
const [tx, ty] = jitteredCellPosition(cell);
return makeParticle(tx, ty);
});
};
const render = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const pointerX = (pointer.x - rect.left) * dpr;
const pointerY = (pointer.y - rect.top) * dpr;
const radius = REPEL_RADIUS_PX * dpr;
const radiusSq = radius * radius;
let allSettled = true;
for (const particle of particles) {
const progress = Math.min(
Math.max(
(elapsed - particle.delay * ASSEMBLE_MS) / (ASSEMBLE_MS * 0.75),
0,
),
1,
);
if (progress < 1) allSettled = false;
const eased = easeOutQuint(progress);
const baseX = particle.sx + (particle.tx - particle.sx) * eased;
const baseY = particle.sy + (particle.ty - particle.sy) * eased;
// wander Lissajous: harmônico principal + secundário em razão
// não-inteira → trajetória contínua sem repetição óbvia
const t = elapsed * particle.speed;
const driftX =
(Math.sin(t + particle.phase) +
0.5 * Math.sin(t * 0.43 + particle.phase * 1.7)) *
particle.amp *
progress;
const driftY =
(Math.cos(t * 0.9 + particle.phase) +
0.5 * Math.cos(t * 0.51 + particle.phase * 2.3)) *
particle.amp *
progress;
const dx = baseX + driftX - pointerX;
const dy = baseY + driftY - pointerY;
const distSq = dx * dx + dy * dy;
if (distSq < radiusSq) {
const dist = Math.sqrt(distSq) || 1;
const force = (1 - dist / radius) * 10 * dpr;
particle.ox += (dx / dist) * force;
particle.oy += (dy / dist) * force;
}
particle.ox *= 0.86;
particle.oy *= 0.86;
const fadeIn = Math.min(elapsed / 1200, 1);
const twinkle = 0.6 + 0.4 * Math.sin(elapsed * 0.003 + particle.phase);
let life = elapsed - particle.birth;
if (life > particle.ttl) {
// renasce em outra célula da grade → uniforme também entre ciclos
const [tx, ty] = jitteredCellPosition(
Math.floor(Math.random() * grid.cols * grid.rows),
);
particle.tx = tx;
particle.ty = ty;
particle.sx = tx;
particle.sy = ty;
particle.phase = Math.random() * Math.PI * 2;
particle.baseAlpha = 0.5 + Math.random() * 0.45;
particle.size = (Math.random() < 0.72 ? 2 : 3) * dpr;
particle.ttl = 4000 + Math.random() * 6000;
particle.birth = elapsed;
life = 0;
}
// fade-in de 800ms ao nascer, fade-out nos últimos 800ms de vida
const lifeFade = Math.min(life / 800, (particle.ttl - life) / 800, 1);
ctx.globalAlpha = Math.min(
1,
particle.baseAlpha * fadeIn * twinkle * lifeFade,
);
ctx.fillStyle = particle.fillStyle;
ctx.fillRect(
baseX + driftX + particle.ox,
baseY + driftY + particle.oy,
particle.size,
particle.size,
);
}
ctx.globalAlpha = 1;
if (allSettled && particles.length > 0) hasAssembled = true;
};
const drawStaticFrame = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (const particle of particles) {
ctx.fillStyle = particle.fillStyle;
ctx.globalAlpha = particle.baseAlpha;
ctx.fillRect(particle.tx, particle.ty, particle.size, particle.size);
}
ctx.globalAlpha = 1;
};
const tick = (now: number) => {
if (!running) return;
elapsed += Math.min(now - lastTime, 50);
lastTime = now;
render();
rafId = window.requestAnimationFrame(tick);
};
const syncLoop = () => {
const shouldRun = !reducedMotion && intersecting && !document.hidden;
if (shouldRun && !running) {
running = true;
lastTime = performance.now();
rafId = window.requestAnimationFrame(tick);
} else if (!shouldRun && running) {
running = false;
window.cancelAnimationFrame(rafId);
}
};
// Warm-up determinístico: corrida de layout pode deixar o container com
// 0px no primeiro sample; re-tenta até existirem partículas
const sampleUntilBuilt = (onBuilt?: () => void) => {
sample();
if (particles.length > 0 || cancelled) {
onBuilt?.();
return;
}
if (sampleAttempts++ > 50) return;
sampleRetryTimer = window.setTimeout(
() => sampleUntilBuilt(onBuilt),
120,
);
};
const scheduleResample = () => {
window.clearTimeout(resampleTimer);
resampleTimer = window.setTimeout(() => {
sampleUntilBuilt(() => {
if (reducedMotion) drawStaticFrame();
});
}, 150);
};
const handlePointerMove = (event: PointerEvent) => {
pointer.x = event.clientX;
pointer.y = event.clientY;
};
const handlePointerLeave = () => {
pointer.x = OFFSCREEN_POINTER;
pointer.y = OFFSCREEN_POINTER;
};
const handleScroll = () => {
rect = container.getBoundingClientRect();
};
const handleVisibilityChange = () => {
syncLoop();
};
const resizeObserver = new ResizeObserver(scheduleResample);
resizeObserver.observe(container);
if (reducedMotion) {
// Variante estática: quadro final montado, sem rAF e sem listeners de ponteiro
hasAssembled = true;
sampleUntilBuilt(() => drawStaticFrame());
return () => {
cancelled = true;
window.clearTimeout(resampleTimer);
window.clearTimeout(sampleRetryTimer);
resizeObserver.disconnect();
};
}
// Start determinístico: intersecting derivado de geometry no setup (o callback do IO
// pode chegar tarde e deixar o loop rAF morto na carga)
const syncIntersecting = () => {
const current = container.getBoundingClientRect();
intersecting =
current.bottom > 0 &&
current.right > 0 &&
current.top < window.innerHeight &&
current.left < window.innerWidth;
};
syncIntersecting();
sampleUntilBuilt(() => syncLoop());
const intersectionObserver = new IntersectionObserver((entries) => {
intersecting = entries.some((entry) => entry.isIntersecting);
syncLoop();
});
intersectionObserver.observe(container);
window.addEventListener("pointermove", handlePointerMove, {
passive: true,
});
document.documentElement.addEventListener(
"pointerleave",
handlePointerLeave,
);
window.addEventListener("scroll", handleScroll, { passive: true });
document.addEventListener("visibilitychange", handleVisibilityChange);
return () => {
cancelled = true;
window.cancelAnimationFrame(rafId);
window.clearTimeout(resampleTimer);
window.clearTimeout(sampleRetryTimer);
resizeObserver.disconnect();
intersectionObserver.disconnect();
window.removeEventListener("pointermove", handlePointerMove);
document.documentElement.removeEventListener(
"pointerleave",
handlePointerLeave,
);
window.removeEventListener("scroll", handleScroll);
document.removeEventListener("visibilitychange", handleVisibilityChange);
};
}, []);
return (
<div
ref={containerRef}
aria-hidden="true"
className="pointer-events-none absolute inset-0"
>
<canvas ref={canvasRef} className="h-full w-full" />
</div>
);
}
@@ -0,0 +1,76 @@
"use client";
import { useEffect, useId, useState } from "react";
interface ScrollVelocityTextProps {
text: string;
direction: "left" | "right";
className?: string;
}
const GROUPS = [0, 1] as const;
export default function ScrollVelocityText({
text,
direction,
className = "",
}: ScrollVelocityTextProps) {
const trackId = useId();
const [isPaused, setIsPaused] = useState(false);
const [shouldReduceMotion, setShouldReduceMotion] = useState(false);
useEffect(() => {
const media = window.matchMedia("(prefers-reduced-motion: reduce)");
setShouldReduceMotion(media.matches);
const updatePreference = (event: MediaQueryListEvent) => {
setShouldReduceMotion(event.matches);
};
media.addEventListener("change", updatePreference);
return () => media.removeEventListener("change", updatePreference);
}, []);
const animationPaused = shouldReduceMotion || isPaused;
return (
<div className={`scroll-velocity-text ${className}`.trim()}>
<div className="scroll-velocity-viewport">
<div
id={trackId}
className={`scroll-velocity-track ${
direction === "left" ? "animate-marquee" : "animate-marquee-reverse"
}`}
style={{
animationPlayState: animationPaused ? "paused" : undefined,
}}
>
{GROUPS.map((group) => (
<span
key={group}
className="scroll-velocity-group"
aria-hidden={group === 0 ? undefined : "true"}
>
<span className="scroll-velocity-item">
<span aria-hidden={group === 0 ? "false" : "true"}>{text}</span>
<span aria-hidden="true" className="scroll-velocity-dot">
&#x2022;
</span>
</span>
</span>
))}
</div>
</div>
{!shouldReduceMotion ? (
<button
type="button"
className="scroll-velocity-control"
aria-controls={trackId}
aria-pressed={isPaused}
onClick={() => setIsPaused((paused) => !paused)}
>
{isPaused ? "Retomar animação" : "Pausar animação"}
</button>
) : null}
</div>
);
}
@@ -0,0 +1,44 @@
interface SectionDividerProps {
variant?: "gradient" | "pulse" | "waveform";
className?: string;
}
const PULSE_DOTS = ["dot-1", "dot-2", "dot-3", "dot-4", "dot-5"] as const;
export default function SectionDivider({
variant = "gradient",
className = "",
}: SectionDividerProps) {
const rootClassName =
`section-divider section-divider-${variant} ${className}`.trim();
if (variant === "gradient") {
return (
<div aria-hidden="true" className={rootClassName}>
<span />
<span />
</div>
);
}
if (variant === "pulse") {
return (
<div aria-hidden="true" className={rootClassName}>
{PULSE_DOTS.map((dot) => (
<span key={dot} />
))}
</div>
);
}
return (
<div aria-hidden="true" className={rootClassName}>
<svg viewBox="0 0 1200 60" preserveAspectRatio="none" focusable="false">
<title>Divisor decorativo</title>
<path d="M0 30 C100 8 180 52 280 30 S460 8 560 30 740 52 840 30 1020 8 1200 30" />
<path d="M0 30 C120 48 220 12 340 30 S560 48 680 30 900 12 1020 30 1140 48 1200 30" />
<path d="M0 30 C80 20 160 40 240 30 S400 20 480 30 640 40 720 30 880 20 960 30 1120 40 1200 30" />
</svg>
</div>
);
}
+193
View File
@@ -0,0 +1,193 @@
"use client";
import { motion, useMotionValue, useSpring, useTransform } from "motion/react";
import { useEffect, useRef, useState } from "react";
import { cn } from "@/lib/utils";
interface TelemetryMetric {
label: string;
value: number;
unit: string;
precision?: number;
min?: number;
max?: number;
}
const METRICS_CONFIG: TelemetryMetric[] = [
{
label: "UPTIME",
value: 99.97,
unit: "%",
precision: 2,
min: 99.9,
max: 100,
},
{
label: "INTEGRAÇÕES",
value: 12,
unit: " ativas",
precision: 0,
min: 8,
max: 16,
},
{
label: "BUILDS",
value: 247,
unit: " /dia",
precision: 0,
min: 200,
max: 300,
},
{ label: "LATÊNCIA", value: 12, unit: " ms", precision: 0, min: 8, max: 28 },
];
const UPDATE_INTERVAL = 3000;
const springConfigs = [
{ stiffness: 300, damping: 30, mass: 0.8 },
{ stiffness: 200, damping: 25, mass: 1 },
{ stiffness: 150, damping: 20, mass: 1.2 },
{ stiffness: 150, damping: 20, mass: 1.2 },
];
export function TelemetryStrip({
className,
reducedMotion = false,
}: {
className?: string;
reducedMotion?: boolean;
}) {
const [_metrics, setMetrics] = useState<TelemetryMetric[]>(METRICS_CONFIG);
const pointerX = useMotionValue(0);
const pointerY = useMotionValue(0);
const isHovering = useRef(false);
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
const metricValue0 = useSpring(
useMotionValue(METRICS_CONFIG[0].value),
springConfigs[0],
);
const metricValue1 = useSpring(
useMotionValue(METRICS_CONFIG[1].value),
springConfigs[1],
);
const metricValue2 = useSpring(
useMotionValue(METRICS_CONFIG[2].value),
springConfigs[2],
);
const metricValue3 = useSpring(
useMotionValue(METRICS_CONFIG[3].value),
springConfigs[3],
);
const metricValues = [metricValue0, metricValue1, metricValue2, metricValue3];
// Parallax sutil em px, sinal alternado por métrica — assinatura "reage ao ponteiro"
const pointerShiftX = useTransform(pointerX, [-1, 1], [-8, 8]);
const pointerShiftY = useTransform(pointerY, [-1, 1], [-5, 5]);
const pointerShiftXNeg = useTransform(pointerShiftX, (v) => -v);
const pointerShiftYNeg = useTransform(pointerShiftY, (v) => -v);
const generateVariation = useRef((metric: TelemetryMetric): number => {
const range =
(metric.max ?? metric.value * 1.1) - (metric.min ?? metric.value * 0.9);
const variation = (Math.random() - 0.5) * range * 0.15;
const newValue = metric.value + variation;
return Math.max(
metric.min ?? -Infinity,
Math.min(metric.max ?? Infinity, newValue),
);
});
const updateMetrics = useRef(() => {
setMetrics((prev) =>
prev.map((metric) => ({
...metric,
value: generateVariation.current(metric),
})),
);
});
useEffect(() => {
if (reducedMotion) return;
intervalRef.current = setInterval(updateMetrics.current, UPDATE_INTERVAL);
return () => {
if (intervalRef.current) clearInterval(intervalRef.current);
};
}, [reducedMotion]);
useEffect(() => {
const handlePointerMove = (e: MouseEvent) => {
const x = (e.clientX / window.innerWidth) * 2 - 1;
const y = -(e.clientY / window.innerHeight) * 2 + 1;
pointerX.set(x);
pointerY.set(y);
};
window.addEventListener("pointermove", handlePointerMove);
return () => window.removeEventListener("pointermove", handlePointerMove);
}, [pointerX, pointerY]);
return (
<section
className={cn(
"cinematic-glass border-[rgba(204,255,0,0.18)] bg-[rgba(10,10,10,0.82)] backdrop-blur-[24px] rounded-[var(--r-lg)] p-sp-4 lg:p-sp-5 grid grid-cols-2 gap-sp-4 lg:gap-sp-6",
className,
)}
aria-label="Telemetria operacional em tempo real"
onPointerEnter={() => {
isHovering.current = true;
}}
onPointerLeave={() => {
isHovering.current = false;
}}
>
{METRICS_CONFIG.map((metric, index) => (
<motion.div
key={metric.label}
className="relative flex flex-col items-start gap-1"
style={
reducedMotion
? undefined
: {
x: index % 2 === 0 ? pointerShiftX : pointerShiftXNeg,
y: index % 2 === 0 ? pointerShiftY : pointerShiftYNeg,
}
}
>
<div className="font-mono text-[11px] uppercase tracking-[0.14em] text-[rgba(244,242,237,.7)]">
{metric.label}
</div>
<div
className="whitespace-nowrap font-display font-extrabold text-[clamp(1.35rem,2.6vw,1.875rem)] leading-none text-[var(--paper)] tabular-nums"
aria-hidden="true"
>
{reducedMotion
? metric.value.toFixed(metric.precision ?? 0)
: metricValues[index].get().toFixed(metric.precision ?? 0)}
<span className="font-mono text-[11px] font-normal text-[rgba(244,242,237,.58)]">
{metric.unit}
</span>
</div>
<div
className="h-px w-full bg-[linear-gradient(90deg,var(--slash),transparent)] opacity-30"
style={{
transformOrigin: "left center",
transform: reducedMotion
? "scaleX(1)"
: `scaleX(${Math.max(0.15, Math.min(1, metric.value / (metric.max ?? 100)))})`,
}}
aria-hidden="true"
/>
</motion.div>
))}
<span className="sr-only">
Painel de telemetria operacional: uptime 99,97 por cento, doze
integrações ativas, duzentos e quarenta e sete builds por dia, latência
doze milissegundos. Valores decorativos simulando uma sala de controle
real.
</span>
</section>
);
}
@@ -16,17 +16,6 @@ function focusFragment(target: HTMLElement) {
target.focus({ preventScroll: true });
}
/**
* A document owns one outstanding homepage arrival. Later mounts are same-document
* history, where the browser restores its own position.
*/
let arrivalSettled = false;
/** Tests replay several documents inside one jsdom document. */
export function resetHomeArrivalState() {
arrivalSettled = false;
}
/** Correct only an outstanding arrival; ordinary links and history keep native scrolling. */
export function useHomeFragmentNavigation() {
const ready = useRef(false);
@@ -37,49 +26,22 @@ export function useHomeFragmentNavigation() {
| PerformanceNavigationTiming
| undefined;
// A history restoration owns its saved position, not the URL's old fragment.
const restoration = navigation?.type === "back_forward";
const arrival = !restoration && !arrivalSettled;
let pending = restoration ? "" : window.location.hash;
let pendingTop = arrival && !window.location.hash;
if (arrival) window.history.scrollRestoration = "manual";
let pending =
navigation?.type === "back_forward" ? "" : window.location.hash;
const resolve = () => {
if (!ready.current) return;
if (pendingTop) {
pendingTop = false;
if (!window.location.hash)
window.scrollTo({ top: 0, left: 0, behavior: "instant" });
}
if (!pending) return;
if (!ready.current || !pending) return;
const hash = pending;
pending = "";
if (hash !== window.location.hash) return;
const target = fragmentTarget(hash);
if (!target) return;
focusFragment(target);
const padding =
Number.parseFloat(
getComputedStyle(document.documentElement).scrollPaddingTop,
) || 0;
const margin =
Number.parseFloat(getComputedStyle(target).scrollMarginTop) || 0;
window.scrollTo({
top: Math.max(
0,
window.scrollY +
target.getBoundingClientRect().top -
padding -
margin,
),
left: 0,
behavior: "instant",
});
target.scrollIntoView({ block: "start", behavior: "instant" });
};
const cancelArrival = () => {
pending = "";
pendingTop = false;
window.history.scrollRestoration = "auto";
};
const onKeyDown = (event: KeyboardEvent) => {
if (
@@ -126,13 +88,11 @@ export function useHomeFragmentNavigation() {
return;
const target = fragmentTarget(url.hash);
if (!target) return;
pendingTop = false;
pending = ready.current ? "" : url.hash;
// Capture before a mobile menu removes its activated link. Do not cancel navigation.
focusFragment(target);
};
const onHashChange = () => {
pendingTop = false;
if (pending && pending !== window.location.hash) cancelArrival();
if (!ready.current) return;
const target = fragmentTarget(window.location.hash);
@@ -149,7 +109,6 @@ export function useHomeFragmentNavigation() {
window.addEventListener("pointerdown", cancelArrival, { passive: true });
return () => {
resolveArrival.current = null;
window.history.scrollRestoration = "auto";
document.removeEventListener("click", onClick, true);
window.removeEventListener("hashchange", onHashChange);
window.removeEventListener("wheel", cancelArrival);
@@ -162,7 +121,5 @@ export function useHomeFragmentNavigation() {
return useCallback(() => {
ready.current = true;
resolveArrival.current?.();
arrivalSettled = true;
window.history.scrollRestoration = "auto";
}, []);
}