added background to panel itself

This commit is contained in:
onysd 2026-09-08 02:02:18 +03:00
parent 87173ae43f
commit f7b3af48de
8 changed files with 122 additions and 58 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -23,8 +23,8 @@
})(); })();
</script> </script>
<script type="module" crossorigin src="/assets/index-C9AiUiug.js"></script> <script type="module" crossorigin src="/assets/index-BdmuVdXP.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-qygZBefQ.css"> <link rel="stylesheet" crossorigin href="/assets/index-dNagKGR1.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View file

@ -0,0 +1,46 @@
import { useEffect, useRef } from "react";
import { BgIcons } from "./BgIcons";
// The product's background: three blurred orbs that drift with the pointer,
// and the field of slowly moving icons over them. Ported from the marketing
// site so the console reads as the same product.
//
// One component for both places it appears -- the sign-in screen and the
// workspace behind every page -- because two copies of a parallax handler is
// how they end up subtly different.
export function AppBackground({ className = "" }: { className?: string }) {
const orbsRef = useRef<HTMLDivElement>(null);
// The orbs drift with the pointer, the same 30px parallax the site uses.
// Written straight to the node instead of through state: this fires on every
// mouse move, and re-rendering the tree for a background offset would be a
// lot of work to move three blurred circles.
useEffect(() => {
// Someone who asked the system for less motion gets none of this. The CSS
// drift is already disabled for them, and a JS-driven transform would walk
// straight past that preference.
if (window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) {
return;
}
function onMove(event: MouseEvent) {
const node = orbsRef.current;
if (!node) return;
const x = (event.clientX / window.innerWidth - 0.5) * 30;
const y = (event.clientY / window.innerHeight - 0.5) * 30;
node.style.transform = `translate(${x}px, ${y}px)`;
}
window.addEventListener("mousemove", onMove);
return () => window.removeEventListener("mousemove", onMove);
}, []);
return (
<div className={`app-background ${className}`.trim()} aria-hidden="true">
<div className="bg-orbs" ref={orbsRef}>
<div className="bg-orb bg-orb--1" />
<div className="bg-orb bg-orb--2" />
<div className="bg-orb bg-orb--3" />
</div>
<BgIcons />
</div>
);
}

View file

@ -37,6 +37,7 @@ import { permissionBotVerificationReview, permissionServerManage, permissionAdmi
import { type Navigate, type RouteState, routeTitle } from "../routing"; import { type Navigate, type RouteState, routeTitle } from "../routing";
import { ThemeSwitch } from "../theme"; import { ThemeSwitch } from "../theme";
import { AddServerLinkModal } from "./AddServerLinkModal"; import { AddServerLinkModal } from "./AddServerLinkModal";
import { AppBackground } from "./AppBackground";
import { AppLink } from "./AppLink"; import { AppLink } from "./AppLink";
// Compresses a sorted (or unsorted) list of layer numbers into run-length // Compresses a sorted (or unsorted) list of layer numbers into run-length
@ -338,6 +339,9 @@ export function Shell({
</header> </header>
<main className="content">{children}</main> <main className="content">{children}</main>
</div> </div>
{/* Behind everything, fixed to the viewport. The sidebar and topbar paint
over it, so it shows through the working area only. */}
<AppBackground className="app-background--workspace" />
</div> </div>
); );
} }

View file

@ -1,7 +1,7 @@
import type { FormEvent } from "react"; import type { FormEvent } from "react";
import { useEffect, useRef, useState } from "react"; import { useEffect, useState } from "react";
import { api, errorMessage } from "../api"; import { api, errorMessage } from "../api";
import { BgIcons } from "../components/BgIcons"; import { AppBackground } from "../components/AppBackground";
import { Alert } from "../components/ui"; import { Alert } from "../components/ui";
import { ThemeSwitch } from "../theme"; import { ThemeSwitch } from "../theme";
import type { AdminSession, PublicBranding } from "../types"; import type { AdminSession, PublicBranding } from "../types";
@ -19,34 +19,11 @@ export function LoginPage({ onLogin }: { onLogin: (session: AdminSession) => voi
// failure so the panel simply keeps its own branding. // failure so the panel simply keeps its own branding.
const [branding, setBranding] = useState<PublicBranding | null>(null); const [branding, setBranding] = useState<PublicBranding | null>(null);
const [iconFailed, setIconFailed] = useState(false); const [iconFailed, setIconFailed] = useState(false);
const orbsRef = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
api.publicBranding().then(setBranding).catch(() => undefined); api.publicBranding().then(setBranding).catch(() => undefined);
}, []); }, []);
// The orbs drift with the pointer, the same 30px parallax the marketing site
// uses. Written straight to the node instead of through state: this fires on
// every mouse move, and re-rendering the whole form for a background offset
// would be a lot of work to move three blurred circles.
useEffect(() => {
// Someone who asked the system for less motion gets none of this: the CSS
// drift is already disabled for them, and a JS-driven transform would walk
// straight past that preference.
if (window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) {
return;
}
function onMove(event: MouseEvent) {
const node = orbsRef.current;
if (!node) return;
const x = (event.clientX / window.innerWidth - 0.5) * 30;
const y = (event.clientY / window.innerHeight - 0.5) * 30;
node.style.transform = `translate(${x}px, ${y}px)`;
}
window.addEventListener("mousemove", onMove);
return () => window.removeEventListener("mousemove", onMove);
}, []);
const serverName = branding?.name?.trim() || "OwpenGram"; const serverName = branding?.name?.trim() || "OwpenGram";
const iconSrc = branding?.has_icon && !iconFailed ? api.publicIconURL() : "/logo.png"; const iconSrc = branding?.has_icon && !iconFailed ? api.publicIconURL() : "/logo.png";
@ -68,12 +45,7 @@ export function LoginPage({ onLogin }: { onLogin: (session: AdminSession) => voi
return ( return (
<main className="login-page"> <main className="login-page">
<div className="bg-orbs" aria-hidden="true" ref={orbsRef}> <AppBackground />
<div className="bg-orb bg-orb--1" />
<div className="bg-orb bg-orb--2" />
<div className="bg-orb bg-orb--3" />
</div>
<BgIcons />
<section className="login-panel"> <section className="login-panel">
<div className="login-head"> <div className="login-head">
<div className="brand brand-elevated"> <div className="brand brand-elevated">

View file

@ -272,7 +272,7 @@
overflow: hidden; overflow: hidden;
} }
.login-page .bg-orbs { .bg-orbs {
position: absolute; position: absolute;
inset: -60px; inset: -60px;
z-index: 0; z-index: 0;
@ -283,14 +283,14 @@
will-change: transform; will-change: transform;
} }
.login-page .bg-orb { .bg-orb {
position: absolute; position: absolute;
border-radius: 50%; border-radius: 50%;
filter: blur(100px); filter: blur(100px);
pointer-events: none; pointer-events: none;
} }
.login-page .bg-orb--1 { .bg-orb--1 {
top: -15%; top: -15%;
left: -10%; left: -10%;
width: 700px; width: 700px;
@ -299,7 +299,7 @@
animation: loginOrbFloat1 20s ease-in-out infinite; animation: loginOrbFloat1 20s ease-in-out infinite;
} }
.login-page .bg-orb--2 { .bg-orb--2 {
top: 25%; top: 25%;
right: -15%; right: -15%;
width: 600px; width: 600px;
@ -308,7 +308,7 @@
animation: loginOrbFloat2 24s ease-in-out infinite; animation: loginOrbFloat2 24s ease-in-out infinite;
} }
.login-page .bg-orb--3 { .bg-orb--3 {
bottom: -15%; bottom: -15%;
left: 30%; left: 30%;
width: 500px; width: 500px;
@ -334,15 +334,15 @@
} }
@media (max-width: 720px) { @media (max-width: 720px) {
.login-page .bg-orb { filter: blur(60px); } .bg-orb { filter: blur(60px); }
.login-page .bg-orb--1 { width: 350px; height: 350px; } .bg-orb--1 { width: 350px; height: 350px; }
.login-page .bg-orb--2 { width: 300px; height: 300px; } .bg-orb--2 { width: 300px; height: 300px; }
.login-page .bg-orb--3 { width: 250px; height: 250px; } .bg-orb--3 { width: 250px; height: 250px; }
} }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.login-page .bg-orb { animation: none; } .bg-orb { animation: none; }
.login-page .bg-orbs { transition: none; } .bg-orbs { transition: none; }
} }
.login-page .login-panel { .login-page .login-panel {
@ -491,9 +491,8 @@
/* The drifting icon field behind the sign-in card. Sits above the blurred orbs /* The drifting icon field behind the sign-in card. Sits above the blurred orbs
and below the panel, so the two background layers read as one scene. and below the panel, so the two background layers read as one scene.
Positioned absolutely inside .login-page rather than fixed like the site's Fills whichever .app-background contains it. */
version, because here it belongs to one screen rather than the whole app. */ .bg-icons {
.login-page .bg-icons {
position: absolute; position: absolute;
inset: 0; inset: 0;
z-index: 0; z-index: 0;
@ -502,9 +501,52 @@
pointer-events: none; pointer-events: none;
} }
.login-page .bg-icon { .bg-icon {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
will-change: transform; will-change: transform;
} }
/* The background wrapper. On the sign-in screen it fills that screen; behind
the workspace it is pinned to the viewport so it does not scroll away under
a long page, and so the icons -- which bounce off window bounds -- stay
inside the area they are drawn in. */
.app-background {
position: absolute;
inset: 0;
z-index: 0;
overflow: hidden;
pointer-events: none;
}
.app-background--workspace {
position: fixed;
}
/* Quieter behind real content than behind a lone sign-in card: the panel is
dense with tables and numbers, and the background has to stay background. */
.app-background--workspace .bg-icons {
opacity: 0.55;
}
.app-background--workspace .bg-orb {
opacity: 0.6;
}
/* The chrome paints over the background: the sidebar hides the part of it that
would otherwise show through the navigation, which is also why the wrapper
can span the whole viewport instead of hard-coding the sidebar's width.
Only z-index is set here -- the sidebar is already sticky, and restating
position would be a chance to change it by accident. The topbar needs
nothing: it carries z-index 20 of its own, and a rule here would out-specify
its own "position: sticky" and quietly stop it sticking. */
.shell > .sidebar {
z-index: 2;
}
.workspace > .content {
position: relative;
z-index: 1;
}