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) {