From a02cf42295af49965c838f1c2cef8ac602b82ab5 Mon Sep 17 00:00:00 2001 From: Daniel Dias Pereira <86617987+DanielDPereira@users.noreply.github.com> Date: Thu, 22 Oct 2026 22:00:41 -0300 Subject: [PATCH 1/9] feat(frontend): add animated graph login background Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- frontend/src/index.css | 97 ++++++++++++++++++++++++++++ frontend/src/views/auth/AuthView.tsx | 59 +++++++++++++++++ 2 files changed, 156 insertions(+) diff --git a/frontend/src/index.css b/frontend/src/index.css index 42c03f1..41234b7 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -3,18 +3,93 @@ @import "./styles/layout.css"; .auth-shell { + position: relative; + isolation: isolate; min-height: 100vh; display: grid; place-items: center; padding: 24px; + overflow: hidden; + background: + radial-gradient(circle at 50% 44%, color-mix(in srgb, var(--brand-primary) 8%, transparent), transparent 38%), + var(--surface-base); +} + +.auth-graph { + position: absolute; + z-index: -1; + inset: 0; + pointer-events: none; + opacity: .9; +} + +.auth-graph::before, +.auth-graph::after { + position: absolute; + content: ""; + width: 34vw; + aspect-ratio: 1; + border-radius: 50%; + background: var(--accent-glow); + filter: blur(80px); + opacity: .22; +} + +.auth-graph::before { + top: -18%; + left: -12%; +} + +.auth-graph::after { + right: -12%; + bottom: -22%; +} + +.auth-graph svg { + display: block; + width: 100%; + height: 100%; +} + +.auth-graph__edges line { + stroke: var(--brand-primary); + stroke-width: 1; + stroke-dasharray: 6 14; + opacity: .26; + animation: auth-graph-edge 7s ease-in-out infinite alternate; + animation-delay: var(--edge-delay); +} + +.auth-graph__nodes circle { + fill: var(--accent-secondary); + opacity: .58; + filter: drop-shadow(0 0 5px var(--brand-primary)); + transform-box: fill-box; + transform-origin: center; + animation: auth-graph-node 4.5s ease-in-out infinite; + animation-delay: var(--node-delay); +} + +@keyframes auth-graph-edge { + from { stroke-dashoffset: 0; opacity: .16; } + to { stroke-dashoffset: -42; opacity: .52; } +} + +@keyframes auth-graph-node { + 0%, 100% { transform: scale(.8); opacity: .36; } + 50% { transform: scale(1.35); opacity: .9; } } .auth-card { + position: relative; + z-index: 1; width: 100%; max-width: 440px; padding: 32px; display: grid; gap: 18px; + box-shadow: 0 24px 80px rgba(0, 0, 0, .34), 0 0 0 1px color-mix(in srgb, var(--brand-primary) 12%, transparent); + backdrop-filter: blur(18px); } .auth-card h1 { @@ -45,6 +120,28 @@ color: var(--status-danger); } +@media (max-width: 600px) { + .auth-shell { + padding: 16px; + } + + .auth-card { + padding: 24px; + } + + .auth-graph { + inset: -8% -28%; + opacity: .7; + } +} + +@media (prefers-reduced-motion: reduce) { + .auth-graph__edges line, + .auth-graph__nodes circle { + animation: none; + } +} + button:focus-visible, input:focus-visible, select:focus-visible, diff --git a/frontend/src/views/auth/AuthView.tsx b/frontend/src/views/auth/AuthView.tsx index 39b2c2c..96c34bd 100644 --- a/frontend/src/views/auth/AuthView.tsx +++ b/frontend/src/views/auth/AuthView.tsx @@ -4,6 +4,64 @@ import { ApiError } from "../../api/api_auth"; import { useAuth } from "../../viewmodels/useAuthViewModel"; import { loginDestination, navigate, safeDestination } from "../../models/navigation"; +const graphNodes = [ + [82, 118], [178, 68], [286, 138], [402, 74], [526, 154], [654, 82], + [780, 132], [918, 64], [1080, 142], [1128, 286], [978, 332], [842, 268], + [704, 370], [566, 292], [418, 382], [274, 300], [126, 382], [42, 524], + [190, 594], [344, 536], [488, 650], [644, 548], [792, 660], [946, 560], + [1102, 672], [1034, 458], [884, 478], [742, 466], [604, 432], [364, 462], +] as const; + +const graphEdges = [ + [0, 1], [0, 2], [1, 2], [1, 3], [2, 3], [2, 5], [3, 4], [3, 6], + [4, 5], [4, 7], [5, 6], [5, 8], [6, 7], [6, 10], [7, 8], [8, 9], + [8, 10], [9, 10], [10, 11], [10, 12], [11, 12], [11, 13], [12, 13], + [12, 14], [13, 14], [13, 15], [14, 15], [14, 19], [15, 16], [15, 17], + [16, 17], [16, 18], [17, 18], [18, 19], [18, 20], [19, 20], [19, 28], + [20, 21], [20, 28], [21, 22], [21, 27], [22, 23], [22, 24], [23, 24], + [23, 25], [24, 25], [24, 26], [25, 26], [26, 27], [26, 29], [27, 28], + [28, 29], [14, 28], [10, 27], +] as const; + +function LoginGraphBackground() { + return ( + + ); +} + export function AuthGate({ children }: { children: ReactNode }) { const { session, notice, restore, logout } = useAuth(); const [location, setLocation] = useState(() => window.location.href); @@ -52,6 +110,7 @@ export function AuthScreen({ initialMode }: { initialMode: "login" | "register" return (
+
{mode === "register" && ( From eec1a81f479624e444c0869a7e29db613dca2975 Mon Sep 17 00:00:00 2001 From: Daniel Dias Pereira <86617987+DanielDPereira@users.noreply.github.com> Date: Thu, 22 Oct 2026 22:18:34 -0300 Subject: [PATCH 2/9] feat(frontend): add 3d mouse interaction to login graph Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- frontend/src/index.css | 31 ++++++++++++++++++++ frontend/src/views/auth/AuthView.tsx | 43 +++++++++++++++++++++++++--- 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/frontend/src/index.css b/frontend/src/index.css index 41234b7..f385a8b 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -3,6 +3,10 @@ @import "./styles/layout.css"; .auth-shell { + --graph-mouse-x: 0px; + --graph-mouse-y: 0px; + --graph-tilt-x: 0deg; + --graph-tilt-y: 0deg; position: relative; isolation: isolate; min-height: 100vh; @@ -49,6 +53,19 @@ display: block; width: 100%; height: 100%; + transform: translate3d(var(--graph-mouse-x), var(--graph-mouse-y), 0) + perspective(900px) rotateX(var(--graph-tilt-x)) rotateY(var(--graph-tilt-y)); + transform-origin: center; + transition: transform 1.2s cubic-bezier(.2, .75, .2, 1); + will-change: transform; +} + +.auth-graph__scene { + width: 100%; + height: 100%; + animation: auth-graph-orbit 26s ease-in-out infinite alternate; + transform-origin: center; + will-change: transform; } .auth-graph__edges line { @@ -80,6 +97,12 @@ 50% { transform: scale(1.35); opacity: .9; } } +@keyframes auth-graph-orbit { + 0% { transform: translate3d(-1.5%, -1%, 0) rotateZ(-1.6deg) scale(1.04); } + 50% { transform: translate3d(1%, 1.5%, 0) rotateZ(1.2deg) scale(1.08); } + 100% { transform: translate3d(-.5%, 2%, 0) rotateZ(2.3deg) scale(1.05); } +} + .auth-card { position: relative; z-index: 1; @@ -136,6 +159,14 @@ } @media (prefers-reduced-motion: reduce) { + .auth-graph__scene { + animation: none; + } + + .auth-graph svg { + transition: none; + } + .auth-graph__edges line, .auth-graph__nodes circle { animation: none; diff --git a/frontend/src/views/auth/AuthView.tsx b/frontend/src/views/auth/AuthView.tsx index 96c34bd..5aea66a 100644 --- a/frontend/src/views/auth/AuthView.tsx +++ b/frontend/src/views/auth/AuthView.tsx @@ -1,5 +1,5 @@ import { Button } from "../common/ui"; -import { useEffect, useState, type ReactNode } from "react"; +import { useEffect, useRef, useState, type ReactNode } from "react"; import { ApiError } from "../../api/api_auth"; import { useAuth } from "../../viewmodels/useAuthViewModel"; import { loginDestination, navigate, safeDestination } from "../../models/navigation"; @@ -26,7 +26,8 @@ const graphEdges = [ function LoginGraphBackground() { return ( ); } @@ -98,18 +100,51 @@ export function AuthGate({ children }: { children: ReactNode }) { export function AuthScreen({ initialMode }: { initialMode: "login" | "register" }) { const [mode, setMode] = useState<"login" | "register">(initialMode); + const authShellRef = useRef(null); + const animationFrame = useRef(null); useEffect(() => { setMode(initialMode); }, [initialMode]); + useEffect(() => { + const updateGraphInteraction = (event: globalThis.MouseEvent) => { + if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return; + + const x = event.clientX / window.innerWidth * 2 - 1; + const y = event.clientY / window.innerHeight * 2 - 1; + if (animationFrame.current !== null) window.cancelAnimationFrame(animationFrame.current); + animationFrame.current = window.requestAnimationFrame(() => { + authShellRef.current?.style.setProperty("--graph-mouse-x", `${x * 18}px`); + authShellRef.current?.style.setProperty("--graph-mouse-y", `${y * 14}px`); + authShellRef.current?.style.setProperty("--graph-tilt-x", `${y * -5}deg`); + authShellRef.current?.style.setProperty("--graph-tilt-y", `${x * 7}deg`); + }); + }; + + const resetGraphInteraction = () => { + authShellRef.current?.style.setProperty("--graph-mouse-x", "0px"); + authShellRef.current?.style.setProperty("--graph-mouse-y", "0px"); + authShellRef.current?.style.setProperty("--graph-tilt-x", "0deg"); + authShellRef.current?.style.setProperty("--graph-tilt-y", "0deg"); + }; + + window.addEventListener("mousemove", updateGraphInteraction); + window.addEventListener("mouseleave", resetGraphInteraction); + return () => { + window.removeEventListener("mousemove", updateGraphInteraction); + window.removeEventListener("mouseleave", resetGraphInteraction); + if (animationFrame.current !== null) window.cancelAnimationFrame(animationFrame.current); + }; + }, []); + const switchMode = (nextMode: "login" | "register") => { setMode(nextMode); navigate(nextMode === "register" ? "/register" : "/login"); }; return ( -
+
From 181a9cfad1575a1141c997e8b7c0511e96288b57 Mon Sep 17 00:00:00 2001 From: Daniel Dias Pereira <86617987+DanielDPereira@users.noreply.github.com> Date: Thu, 22 Oct 2026 22:22:55 -0300 Subject: [PATCH 3/9] feat(frontend): rotate login graph clusters in 3d Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- frontend/src/index.css | 63 +++++++++++++++++++++------- frontend/src/views/auth/AuthView.tsx | 52 +++++++++++++---------- 2 files changed, 78 insertions(+), 37 deletions(-) diff --git a/frontend/src/index.css b/frontend/src/index.css index f385a8b..e85601f 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -53,21 +53,49 @@ display: block; width: 100%; height: 100%; - transform: translate3d(var(--graph-mouse-x), var(--graph-mouse-y), 0) - perspective(900px) rotateX(var(--graph-tilt-x)) rotateY(var(--graph-tilt-y)); - transform-origin: center; - transition: transform 1.2s cubic-bezier(.2, .75, .2, 1); - will-change: transform; } .auth-graph__scene { width: 100%; height: 100%; - animation: auth-graph-orbit 26s ease-in-out infinite alternate; + perspective: 900px; + transform-style: preserve-3d; +} + +.auth-graph__cluster { + transform-box: fill-box; transform-origin: center; + transform-style: preserve-3d; will-change: transform; } +.auth-graph__cluster-content { + transform-box: fill-box; + transform-origin: center; + transform: translate3d( + var(--graph-mouse-x), + var(--graph-mouse-y), + 0 + ) + perspective(700px) + rotateX(var(--graph-tilt-x)) + rotateY(var(--graph-tilt-y)); + transition: transform 1s cubic-bezier(.2, .75, .2, 1); + will-change: transform; +} + +.auth-graph__cluster--1 { + animation: auth-graph-cluster-one 24s ease-in-out infinite alternate; +} + +.auth-graph__cluster--2 { + animation: auth-graph-cluster-two 29s ease-in-out infinite alternate-reverse; +} + +.auth-graph__cluster--3 { + animation: auth-graph-cluster-three 21s ease-in-out infinite alternate; +} + .auth-graph__edges line { stroke: var(--brand-primary); stroke-width: 1; @@ -97,10 +125,19 @@ 50% { transform: scale(1.35); opacity: .9; } } -@keyframes auth-graph-orbit { - 0% { transform: translate3d(-1.5%, -1%, 0) rotateZ(-1.6deg) scale(1.04); } - 50% { transform: translate3d(1%, 1.5%, 0) rotateZ(1.2deg) scale(1.08); } - 100% { transform: translate3d(-.5%, 2%, 0) rotateZ(2.3deg) scale(1.05); } +@keyframes auth-graph-cluster-one { + 0% { transform: rotateX(-5deg) rotateY(-8deg) rotateZ(-2deg) translate3d(-8px, -5px, 0); } + 100% { transform: rotateX(7deg) rotateY(10deg) rotateZ(3deg) translate3d(8px, 7px, 18px); } +} + +@keyframes auth-graph-cluster-two { + 0% { transform: rotateX(8deg) rotateY(7deg) rotateZ(2deg) translate3d(7px, 4px, 12px); } + 100% { transform: rotateX(-7deg) rotateY(-11deg) rotateZ(-3deg) translate3d(-9px, -6px, -8px); } +} + +@keyframes auth-graph-cluster-three { + 0% { transform: rotateX(-6deg) rotateY(10deg) rotateZ(1deg) translate3d(-6px, 8px, -6px); } + 100% { transform: rotateX(8deg) rotateY(-8deg) rotateZ(-2deg) translate3d(9px, -7px, 16px); } } .auth-card { @@ -159,11 +196,9 @@ } @media (prefers-reduced-motion: reduce) { - .auth-graph__scene { + .auth-graph__cluster, + .auth-graph__cluster-content { animation: none; - } - - .auth-graph svg { transition: none; } diff --git a/frontend/src/views/auth/AuthView.tsx b/frontend/src/views/auth/AuthView.tsx index 5aea66a..c9b31bb 100644 --- a/frontend/src/views/auth/AuthView.tsx +++ b/frontend/src/views/auth/AuthView.tsx @@ -35,29 +35,35 @@ function LoginGraphBackground() { - - {graphEdges.map(([from, to], index) => ( - - ))} - - - {graphNodes.map(([cx, cy], index) => ( - - ))} - + {[graphEdges.slice(0, 18), graphEdges.slice(18, 36), graphEdges.slice(36)].map((edges, clusterIndex) => ( + + + + {edges.map(([from, to], index) => ( + + ))} + + + {graphNodes.slice(clusterIndex * 10, clusterIndex === 2 ? undefined : clusterIndex * 10 + 12).map(([cx, cy], index) => ( + + ))} + + + + ))}
From f6a8463d4589e2a49ea5475c0081dfda43f416cd Mon Sep 17 00:00:00 2001 From: Daniel Dias Pereira <86617987+DanielDPereira@users.noreply.github.com> Date: Thu, 22 Oct 2026 22:25:41 -0300 Subject: [PATCH 4/9] feat(frontend): smooth graph axis rotation and scoped mouse input Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- frontend/src/index.css | 26 ++++++++++----------- frontend/src/views/auth/AuthView.tsx | 35 +++++++++++++++------------- 2 files changed, 32 insertions(+), 29 deletions(-) diff --git a/frontend/src/index.css b/frontend/src/index.css index e85601f..9ee8870 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -3,10 +3,6 @@ @import "./styles/layout.css"; .auth-shell { - --graph-mouse-x: 0px; - --graph-mouse-y: 0px; - --graph-tilt-x: 0deg; - --graph-tilt-y: 0deg; position: relative; isolation: isolate; min-height: 100vh; @@ -20,6 +16,10 @@ } .auth-graph { + --graph-mouse-x: 0px; + --graph-mouse-y: 0px; + --graph-tilt-x: 0deg; + --graph-tilt-y: 0deg; position: absolute; z-index: -1; inset: 0; @@ -85,15 +85,15 @@ } .auth-graph__cluster--1 { - animation: auth-graph-cluster-one 24s ease-in-out infinite alternate; + animation: auth-graph-cluster-one 42s linear infinite; } .auth-graph__cluster--2 { - animation: auth-graph-cluster-two 29s ease-in-out infinite alternate-reverse; + animation: auth-graph-cluster-two 58s linear infinite reverse; } .auth-graph__cluster--3 { - animation: auth-graph-cluster-three 21s ease-in-out infinite alternate; + animation: auth-graph-cluster-three 50s linear infinite; } .auth-graph__edges line { @@ -126,18 +126,18 @@ } @keyframes auth-graph-cluster-one { - 0% { transform: rotateX(-5deg) rotateY(-8deg) rotateZ(-2deg) translate3d(-8px, -5px, 0); } - 100% { transform: rotateX(7deg) rotateY(10deg) rotateZ(3deg) translate3d(8px, 7px, 18px); } + from { transform: rotateZ(0deg) rotateX(-4deg) rotateY(-7deg); } + to { transform: rotateZ(360deg) rotateX(-4deg) rotateY(-7deg); } } @keyframes auth-graph-cluster-two { - 0% { transform: rotateX(8deg) rotateY(7deg) rotateZ(2deg) translate3d(7px, 4px, 12px); } - 100% { transform: rotateX(-7deg) rotateY(-11deg) rotateZ(-3deg) translate3d(-9px, -6px, -8px); } + from { transform: rotateZ(0deg) rotateX(7deg) rotateY(8deg); } + to { transform: rotateZ(360deg) rotateX(7deg) rotateY(8deg); } } @keyframes auth-graph-cluster-three { - 0% { transform: rotateX(-6deg) rotateY(10deg) rotateZ(1deg) translate3d(-6px, 8px, -6px); } - 100% { transform: rotateX(8deg) rotateY(-8deg) rotateZ(-2deg) translate3d(9px, -7px, 16px); } + from { transform: rotateZ(0deg) rotateX(-6deg) rotateY(10deg); } + to { transform: rotateZ(360deg) rotateX(-6deg) rotateY(10deg); } } .auth-card { diff --git a/frontend/src/views/auth/AuthView.tsx b/frontend/src/views/auth/AuthView.tsx index c9b31bb..f9b8132 100644 --- a/frontend/src/views/auth/AuthView.tsx +++ b/frontend/src/views/auth/AuthView.tsx @@ -1,5 +1,5 @@ import { Button } from "../common/ui"; -import { useEffect, useRef, useState, type ReactNode } from "react"; +import { useEffect, useRef, useState, type ReactNode, type RefObject } from "react"; import { ApiError } from "../../api/api_auth"; import { useAuth } from "../../viewmodels/useAuthViewModel"; import { loginDestination, navigate, safeDestination } from "../../models/navigation"; @@ -23,9 +23,9 @@ const graphEdges = [ [28, 29], [14, 28], [10, 27], ] as const; -function LoginGraphBackground() { +function LoginGraphBackground({ graphRef }: { graphRef?: RefObject }) { return ( -