diff --git a/packages/app/e2e/browser/onboarding.spec.ts b/packages/app/e2e/browser/onboarding.spec.ts
index 7ba2b51b89..cf9ab06442 100644
--- a/packages/app/e2e/browser/onboarding.spec.ts
+++ b/packages/app/e2e/browser/onboarding.spec.ts
@@ -120,6 +120,58 @@ test("a new user goes from the loading screen straight into setup", async ({ pag
}
});
+test("with reduced motion, setup appears without fading in", async ({ page }) => {
+ const host = await startSetupHost("reduced-motion");
+
+ try {
+ await page.emulateMedia({ reducedMotion: "reduce" });
+ await recordFirstLaunch(page);
+ await openAppAsNewUser(page, host);
+ await expectSetupStep(page, "Welcome to Paseo");
+
+ expect((await readFirstLaunch(page)).setupOpacityOnArrival).toBe(1);
+ } finally {
+ await host.close();
+ }
+});
+
+test("changing the theme keeps the setup window in place on every step", async ({ page }) => {
+ const host = await startSetupHost("theme-change");
+
+ try {
+ await openAppAsNewUser(page, host);
+ await expectSetupStep(page, "Welcome to Paseo");
+ const setupWindow = await setupModal(page).elementHandle();
+
+ for (const title of [
+ "Welcome to Paseo",
+ "Your agents",
+ "Add your projects",
+ "Let agents use Paseo",
+ "Take your agents with you",
+ ]) {
+ if (title !== "Welcome to Paseo") await continueSetup(page);
+ await expectSetupStep(page, title);
+ const heading = setupModal(page).getByRole("heading", { name: title });
+ const headingNode = await heading.elementHandle();
+ const lightColor = await heading.evaluate((node) => getComputedStyle(node).color);
+ await page.emulateMedia({ colorScheme: "dark" });
+ await expect(heading).not.toHaveCSS("color", lightColor);
+ await page.emulateMedia({ colorScheme: "light" });
+ await expect(heading).toHaveCSS("color", lightColor);
+ expect(await setupWindow?.evaluate((node) => node.isConnected), title).toBe(true);
+ expect(await headingNode?.evaluate((node) => node.isConnected), title).toBe(true);
+ }
+
+ await goToSetupStep(page, "Appearance");
+ await pickTheme(page, "Midnight");
+ await pickTheme(page, "Light");
+ expect(await setupWindow?.evaluate((node) => node.isConnected)).toBe(true);
+ } finally {
+ await host.close();
+ }
+});
+
test("the loading screen stays up while the setup host loads its workspaces", async ({ page }) => {
const host = await startSetupHost("slow-workspaces");
const gate = await installDaemonWebSocketGate(page, { port: host.port });
diff --git a/packages/app/src/app/_layout.tsx b/packages/app/src/app/_layout.tsx
index cda1dc78ac..8548b4bb76 100644
--- a/packages/app/src/app/_layout.tsx
+++ b/packages/app/src/app/_layout.tsx
@@ -558,7 +558,6 @@ function AppContainer({ children, chromeEnabled: chromeEnabledOverride }: AppCon
-
@@ -569,6 +568,10 @@ function AppContainer({ children, chromeEnabled: chromeEnabledOverride }: AppCon
+ {/* Setup changes the theme while it is open, so it stays out of the remount above. Its
+ colors are CSS variables and themed props, which update in place, and setup changes no
+ font size. */}
+
);
diff --git a/packages/app/src/components/adaptive-modal-sheet.tsx b/packages/app/src/components/adaptive-modal-sheet.tsx
index ba27d4c9dd..7e28f3d81a 100644
--- a/packages/app/src/components/adaptive-modal-sheet.tsx
+++ b/packages/app/src/components/adaptive-modal-sheet.tsx
@@ -237,21 +237,23 @@ const WEB_ENTER_ANIMATION_NAME = "paseo-modal-enter";
// The desktop card fades in; the dim lands at once, so the screen behind is never seen undimmed
// while the dialog arrives. A CSS animation runs from the card's first paint, so a dialog that opens
-// on its own, like first-run setup, never pops in. Closing fades the whole overlay.
+// on its own, like first-run setup, never pops in. Closing fades the whole overlay. With reduced
+// motion the keyframes are never defined, so the card appears at once.
function ensureWebEnterKeyframes() {
if (!isWeb || typeof document === "undefined") return;
if (document.getElementById(WEB_ENTER_ANIMATION_NAME)) return;
const style = document.createElement("style");
style.id = WEB_ENTER_ANIMATION_NAME;
- style.textContent = `@keyframes ${WEB_ENTER_ANIMATION_NAME} { from { opacity: 0; } to { opacity: 1; } }`;
+ style.textContent = `@media (prefers-reduced-motion: no-preference) { @keyframes ${WEB_ENTER_ANIMATION_NAME} { from { opacity: 0; } to { opacity: 1; } } }`;
document.head.appendChild(style);
}
ensureWebEnterKeyframes();
+// The CSS animation props are not part of ViewStyle without the generated expo-env.d.ts.
const WEB_ENTER_STYLE = {
animationName: WEB_ENTER_ANIMATION_NAME,
animationDuration: `${WEB_ENTER_DURATION_MS}ms`,
animationTimingFunction: "ease-out",
-};
+} as unknown as ViewStyle;
/** The compact sheet's surface: Gorhom's `backgroundComponent` for every bottom sheet. */
export function SheetBackground({ style }: BottomSheetBackgroundProps) {