mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(desktop): render black static boot screen (#1570)
Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
co-authored by
npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
parent
426f044844
commit
10444d248e
@@ -5,6 +5,19 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/buzz.svg?v=20260610" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title></title>
|
||||
<!--
|
||||
Boot background: the document must be black from its very first paint,
|
||||
before the app bundle (and its stylesheets) load, so cold boot never
|
||||
flashes white ahead of the loading gate. The window itself is also
|
||||
black pre-document via `backgroundColor` in tauri.conf.json. The body
|
||||
paints its own themed background once the app CSS loads, so this never
|
||||
shows through after boot.
|
||||
-->
|
||||
<style>
|
||||
html {
|
||||
background-color: #000;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"height": 600,
|
||||
"maximized": true,
|
||||
"visible": false,
|
||||
"backgroundColor": "#000000",
|
||||
"titleBarStyle": "Overlay",
|
||||
"hiddenTitle": true,
|
||||
"dragDropEnabled": false,
|
||||
|
||||
+7
-13
@@ -12,7 +12,6 @@ import {
|
||||
} from "react";
|
||||
|
||||
import { router } from "@/app/router";
|
||||
import { ThemeGrainientBackground } from "@/app/ThemeGrainientBackground";
|
||||
import { useReloadShortcut } from "@/app/useReloadShortcut";
|
||||
import { useAppOnboardingState } from "@/features/onboarding/hooks";
|
||||
import { OnboardingSlideTransition } from "@/features/onboarding/ui/OnboardingSlideTransition";
|
||||
@@ -27,31 +26,26 @@ import { isSharedIdentity as isSharedIdentityCmd } from "@/shared/api/tauri";
|
||||
import { listenForDeepLinks } from "@/shared/deep-link";
|
||||
import { useSystemColorScheme } from "@/shared/theme/useSystemColorScheme";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { BuzzMark } from "@/shared/ui/buzz-logo/BuzzMark";
|
||||
import { Spinner } from "@/shared/ui/spinner";
|
||||
import { StartupWindowDragRegion } from "@/shared/ui/StartupWindowDragRegion";
|
||||
import { StepProgress } from "@/shared/ui/step-progress";
|
||||
|
||||
const LOADING_TEXT = "Setting up your workspace...";
|
||||
|
||||
// Cold boot gate: a plain static Buzz mark (#D7D72E) on solid black. No
|
||||
// animation machinery, no gradient — the mark must paint complete on the very
|
||||
// first frame, even in webviews that render before scripting or SMIL start.
|
||||
function AppLoadingGate() {
|
||||
return (
|
||||
<div
|
||||
className="buzz-setup-loading-shell flex min-h-dvh flex-col items-center justify-center overflow-hidden px-6 py-10"
|
||||
className="flex min-h-dvh flex-col items-center justify-center overflow-hidden bg-black px-6 py-10 text-[#d7d72e]"
|
||||
data-testid="app-loading-gate"
|
||||
role="status"
|
||||
>
|
||||
<StartupWindowDragRegion />
|
||||
<ThemeGrainientBackground />
|
||||
|
||||
<h1
|
||||
aria-live="polite"
|
||||
className="relative z-10 mt-6 text-center text-3xl font-semibold text-foreground"
|
||||
>
|
||||
<span className="sr-only">{LOADING_TEXT}</span>
|
||||
<span aria-hidden="true" className="buzz-setup-loading-text">
|
||||
{LOADING_TEXT}
|
||||
</span>
|
||||
</h1>
|
||||
<span className="sr-only">{LOADING_TEXT}</span>
|
||||
<BuzzMark className="h-auto w-28" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
export function ThemeGrainientBackground() {
|
||||
return (
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="buzz-setup-grainient"
|
||||
data-testid="setup-grainient-background"
|
||||
>
|
||||
<div className="buzz-setup-grainient__wash" />
|
||||
<div className="buzz-setup-grainient__veil" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -338,234 +338,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
@property --buzz-grainient-x-0 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 85%;
|
||||
}
|
||||
|
||||
@property --buzz-grainient-y-0 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 80%;
|
||||
}
|
||||
|
||||
@property --buzz-grainient-x-1 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 60%;
|
||||
}
|
||||
|
||||
@property --buzz-grainient-y-1 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 24%;
|
||||
}
|
||||
|
||||
@property --buzz-grainient-x-2 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 13%;
|
||||
}
|
||||
|
||||
@property --buzz-grainient-y-2 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 82%;
|
||||
}
|
||||
|
||||
@property --buzz-grainient-x-3 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 24%;
|
||||
}
|
||||
|
||||
@property --buzz-grainient-y-3 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 7%;
|
||||
}
|
||||
|
||||
.buzz-setup-loading-shell {
|
||||
--buzz-grainient-alpha-strong: 0.46;
|
||||
--buzz-grainient-alpha-medium: 0.34;
|
||||
--buzz-grainient-alpha-soft: 0.24;
|
||||
--buzz-setup-loading-text-color: color-mix(
|
||||
in srgb,
|
||||
hsl(var(--foreground)) 88%,
|
||||
hsl(var(--background)) 12%
|
||||
);
|
||||
--buzz-grainient-color-0: hsl(
|
||||
var(--chart-5) /
|
||||
var(--buzz-grainient-alpha-strong)
|
||||
);
|
||||
--buzz-grainient-color-1: hsl(
|
||||
var(--chart-3) /
|
||||
var(--buzz-grainient-alpha-strong)
|
||||
);
|
||||
--buzz-grainient-color-2: hsl(
|
||||
var(--primary) /
|
||||
var(--buzz-grainient-alpha-medium)
|
||||
);
|
||||
--buzz-grainient-color-3: hsl(
|
||||
var(--chart-2) /
|
||||
var(--buzz-grainient-alpha-strong)
|
||||
);
|
||||
background-color: hsl(var(--background));
|
||||
color: hsl(var(--foreground));
|
||||
isolation: isolate;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dark .buzz-setup-loading-shell {
|
||||
--buzz-grainient-alpha-strong: 0.68;
|
||||
--buzz-grainient-alpha-medium: 0.5;
|
||||
--buzz-grainient-alpha-soft: 0.34;
|
||||
--buzz-setup-loading-text-color: color-mix(
|
||||
in srgb,
|
||||
hsl(var(--foreground)) 94%,
|
||||
hsl(var(--background)) 6%
|
||||
);
|
||||
}
|
||||
|
||||
.buzz-setup-grainient {
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.buzz-setup-grainient__wash,
|
||||
.buzz-setup-grainient__veil {
|
||||
inset: 0;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.buzz-setup-grainient__wash {
|
||||
--buzz-grainient-s-start-0: 9%;
|
||||
--buzz-grainient-s-end-0: 55%;
|
||||
--buzz-grainient-s-start-1: 5%;
|
||||
--buzz-grainient-s-end-1: 72%;
|
||||
--buzz-grainient-s-start-2: 5%;
|
||||
--buzz-grainient-s-end-2: 52%;
|
||||
--buzz-grainient-s-start-3: 13%;
|
||||
--buzz-grainient-s-end-3: 68%;
|
||||
--buzz-grainient-x-0: 85%;
|
||||
--buzz-grainient-y-0: 80%;
|
||||
--buzz-grainient-x-1: 60%;
|
||||
--buzz-grainient-y-1: 24%;
|
||||
--buzz-grainient-x-2: 13%;
|
||||
--buzz-grainient-y-2: 82%;
|
||||
--buzz-grainient-x-3: 24%;
|
||||
--buzz-grainient-y-3: 7%;
|
||||
animation: buzz-grainient-orbit 10s linear infinite alternate;
|
||||
background-color: hsl(var(--background));
|
||||
background-image:
|
||||
radial-gradient(
|
||||
circle at var(--buzz-grainient-x-0) var(--buzz-grainient-y-0),
|
||||
var(--buzz-grainient-color-0) var(--buzz-grainient-s-start-0),
|
||||
transparent var(--buzz-grainient-s-end-0)
|
||||
),
|
||||
radial-gradient(
|
||||
circle at var(--buzz-grainient-x-1) var(--buzz-grainient-y-1),
|
||||
var(--buzz-grainient-color-1) var(--buzz-grainient-s-start-1),
|
||||
transparent var(--buzz-grainient-s-end-1)
|
||||
),
|
||||
radial-gradient(
|
||||
circle at var(--buzz-grainient-x-2) var(--buzz-grainient-y-2),
|
||||
var(--buzz-grainient-color-2) var(--buzz-grainient-s-start-2),
|
||||
transparent var(--buzz-grainient-s-end-2)
|
||||
),
|
||||
radial-gradient(
|
||||
circle at var(--buzz-grainient-x-3) var(--buzz-grainient-y-3),
|
||||
var(--buzz-grainient-color-3) var(--buzz-grainient-s-start-3),
|
||||
transparent var(--buzz-grainient-s-end-3)
|
||||
);
|
||||
background-blend-mode: normal, normal, normal, normal;
|
||||
contain: paint;
|
||||
filter: saturate(1.08);
|
||||
transform: translateZ(0);
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
.buzz-setup-grainient__veil {
|
||||
background:
|
||||
radial-gradient(
|
||||
ellipse 74% 54% at 50% 52%,
|
||||
transparent 0%,
|
||||
hsl(var(--background) / 0.04) 72%,
|
||||
hsl(var(--background) / 0.16) 100%
|
||||
),
|
||||
linear-gradient(
|
||||
180deg,
|
||||
hsl(var(--background) / 0.04),
|
||||
hsl(var(--background) / 0.12)
|
||||
);
|
||||
}
|
||||
|
||||
.buzz-setup-loading-text {
|
||||
animation: buzz-setup-character-rise 700ms cubic-bezier(0.2, 0.8, 0.2, 1) both;
|
||||
backface-visibility: hidden;
|
||||
color: var(--buzz-setup-loading-text-color);
|
||||
display: inline-block;
|
||||
opacity: 0;
|
||||
text-shadow:
|
||||
0 1px 2px hsl(var(--background) / 0.62),
|
||||
0 14px 34px hsl(var(--background) / 0.5);
|
||||
transform: translate3d(0, 32px, 0);
|
||||
transform-origin: 50% 55%;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
@keyframes buzz-grainient-orbit {
|
||||
0% {
|
||||
--buzz-grainient-x-0: 85%;
|
||||
--buzz-grainient-y-0: 80%;
|
||||
--buzz-grainient-x-1: 60%;
|
||||
--buzz-grainient-y-1: 24%;
|
||||
--buzz-grainient-x-2: 13%;
|
||||
--buzz-grainient-y-2: 82%;
|
||||
--buzz-grainient-x-3: 24%;
|
||||
--buzz-grainient-y-3: 7%;
|
||||
}
|
||||
|
||||
100% {
|
||||
--buzz-grainient-x-0: 31%;
|
||||
--buzz-grainient-y-0: 94%;
|
||||
--buzz-grainient-x-1: 2%;
|
||||
--buzz-grainient-y-1: 25%;
|
||||
--buzz-grainient-x-2: 98%;
|
||||
--buzz-grainient-y-2: 20%;
|
||||
--buzz-grainient-x-3: 95%;
|
||||
--buzz-grainient-y-3: 92%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes buzz-setup-character-rise {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, 32px, 0);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.buzz-setup-grainient__wash {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.buzz-setup-loading-text {
|
||||
animation: none;
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
.buzz-avatar-hue-scrubber {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { useId } from "react";
|
||||
|
||||
/**
|
||||
* The finished Buzz mark as a plain static SVG — no SMIL, no scripting, no
|
||||
* animation machinery. Geometry matches the final keyframe of the
|
||||
* BuzzLogoAnimation morph (v8 variant), rendered in `currentColor`, so it
|
||||
* paints complete on the very first frame regardless of animation support.
|
||||
*/
|
||||
export function BuzzMark({ className }: { className?: string }) {
|
||||
const maskId = `buzz-mark-cutouts-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
|
||||
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className={["buzz-mark", className].filter(Boolean).join(" ")}
|
||||
viewBox="0 0 466 309"
|
||||
fill="currentColor"
|
||||
>
|
||||
<defs>
|
||||
<mask
|
||||
id={maskId}
|
||||
x="-80"
|
||||
y="-80"
|
||||
width="626"
|
||||
height="469"
|
||||
maskUnits="userSpaceOnUse"
|
||||
maskContentUnits="userSpaceOnUse"
|
||||
>
|
||||
<rect x="-80" y="-80" width="626" height="469" fill="#fff" />
|
||||
<ellipse cx="193.3" cy="84.4" rx="27" ry="27" fill="#000" />
|
||||
<ellipse cx="276" cy="84.4" rx="27" ry="27" fill="#000" />
|
||||
<rect
|
||||
x="166.3"
|
||||
y="157.2"
|
||||
width="136.9"
|
||||
height="38.3"
|
||||
rx="5"
|
||||
fill="#000"
|
||||
/>
|
||||
<rect
|
||||
x="166.9"
|
||||
y="235.1"
|
||||
width="136.2"
|
||||
height="37.6"
|
||||
rx="5"
|
||||
fill="#000"
|
||||
/>
|
||||
</mask>
|
||||
</defs>
|
||||
<g mask={`url(#${maskId})`}>
|
||||
<circle cx="91.7" cy="154.5" r="91.7" />
|
||||
<circle cx="374.3" cy="154.5" r="91.7" />
|
||||
<rect x="128" y="0" width="210" height="309" rx="34" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1054,7 +1054,7 @@ test("first-run onboarding shows setup loading until Welcome bootstrap completes
|
||||
await seedActiveIdentity(page, BLANK_TYLER_IDENTITY);
|
||||
await installMockBridge(
|
||||
page,
|
||||
{ createManagedAgentDelayMs: 5_000 },
|
||||
{ createManagedAgentDelayMs: 9_000 },
|
||||
{ skipOnboardingSeed: true },
|
||||
);
|
||||
await page.goto("/");
|
||||
@@ -1067,47 +1067,45 @@ test("first-run onboarding shows setup loading until Welcome bootstrap completes
|
||||
await expect(page.getByTestId("onboarding-gate")).toHaveCount(0);
|
||||
await expect(loadingGate).toBeVisible();
|
||||
await expect(loadingGate).toContainText("Setting up your workspace...");
|
||||
await expect(
|
||||
loadingGate.getByTestId("setup-grainient-background"),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
loadingGate.getByTestId("setup-grainient-background").locator("canvas"),
|
||||
).toHaveCount(0);
|
||||
await expect
|
||||
.poll(async () =>
|
||||
loadingGate.evaluate((element) => {
|
||||
const wash = element.querySelector(".buzz-setup-grainient__wash");
|
||||
if (!(wash instanceof HTMLElement)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const shellStyles = window.getComputedStyle(element);
|
||||
const washStyles = window.getComputedStyle(wash);
|
||||
const loadingText = element.querySelector(".buzz-setup-loading-text");
|
||||
const textStyles =
|
||||
loadingText instanceof HTMLElement
|
||||
? window.getComputedStyle(loadingText)
|
||||
: null;
|
||||
return {
|
||||
animationName: washStyles.animationName,
|
||||
backgroundMatchesTheme:
|
||||
shellStyles.backgroundColor === washStyles.backgroundColor,
|
||||
textAvoidsHardcodedWhite: textStyles?.color !== "rgb(255, 255, 255)",
|
||||
usesRadialGradients:
|
||||
washStyles.backgroundImage.includes("radial-gradient"),
|
||||
};
|
||||
}),
|
||||
)
|
||||
.toEqual({
|
||||
animationName: "buzz-grainient-orbit",
|
||||
backgroundMatchesTheme: true,
|
||||
textAvoidsHardcodedWhite: true,
|
||||
usesRadialGradients: true,
|
||||
});
|
||||
// The boot gate is deliberately static: a plain Buzz mark in the brand
|
||||
// yellow (#D7D72E) over solid black. The mark must paint complete on the
|
||||
// FIRST frame — a blank gate reads as "nothing is loading" — so nothing
|
||||
// about it may depend on SMIL/scripted animation, and the background must
|
||||
// be a flat color rather than the animated gradient wash.
|
||||
const mark = loadingGate.locator(".buzz-mark");
|
||||
await expect(mark).toBeVisible();
|
||||
const gateTreatment = await loadingGate.evaluate((element) => {
|
||||
const shellStyles = window.getComputedStyle(element);
|
||||
const markSvg = element.querySelector(".buzz-mark");
|
||||
const markStyles =
|
||||
markSvg instanceof SVGElement ? window.getComputedStyle(markSvg) : null;
|
||||
return {
|
||||
animateElementCount: element.querySelectorAll("animate").length,
|
||||
backgroundColor: shellStyles.backgroundColor,
|
||||
backgroundImage: shellStyles.backgroundImage,
|
||||
// The document itself must also be black (inline <style> in
|
||||
// index.html) so the pre-React/pre-CSS first paint can't flash white
|
||||
// before the gate mounts.
|
||||
documentBackgroundColor: window.getComputedStyle(document.documentElement)
|
||||
.backgroundColor,
|
||||
markColor: markStyles?.color,
|
||||
markUsesCurrentColor: markSvg?.getAttribute("fill") === "currentColor",
|
||||
};
|
||||
});
|
||||
expect(gateTreatment).toEqual({
|
||||
animateElementCount: 0,
|
||||
backgroundColor: "rgb(0, 0, 0)",
|
||||
backgroundImage: "none",
|
||||
documentBackgroundColor: "rgb(0, 0, 0)",
|
||||
markColor: "rgb(215, 215, 46)", // #d7d72e
|
||||
markUsesCurrentColor: true,
|
||||
});
|
||||
await expect(loadingGate).not.toHaveClass(/buzz-onboarding-neutral-theme/);
|
||||
await expectShellHidden(page);
|
||||
await page.waitForTimeout(250);
|
||||
await expect(loadingGate).toBeVisible();
|
||||
await expect(mark).toBeVisible();
|
||||
|
||||
await expectWelcomeView(page);
|
||||
await expectPrivateWelcomeChannel(page);
|
||||
|
||||
Reference in New Issue
Block a user