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:
Taylor Ho
2026-07-07 08:38:00 -07:00
committed by GitHub
co-authored by npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
parent 426f044844
commit 10444d248e
7 changed files with 113 additions and 290 deletions
+13
View File
@@ -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>
+1
View File
@@ -21,6 +21,7 @@
"height": 600,
"maximized": true,
"visible": false,
"backgroundColor": "#000000",
"titleBarStyle": "Overlay",
"hiddenTitle": true,
"dragDropEnabled": false,
+7 -13
View File
@@ -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>
);
}
+35 -37
View File
@@ -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);