fix(web): show settings on first load and hoist DemoProvider globally

Bug 1: On a fresh load with no saved config, RootLayout returned `null`
while a useEffect-driven `router.navigate()` fired, leaving a blank screen
until the user manually refreshed. Move the redirect into the root route's
`beforeLoad` so it happens synchronously during route resolution and the
settings form renders on first paint.

Bug 2: `DemoProvider` was mounted inside `RootLayout` only on the
non-settings branch, so any component reading `useDemo()` outside that
branch would throw "useDemoContext must be used within DemoProvider".
Hoist `<DemoProvider>` to `main.tsx` so the context is available app-wide.

Adds vitest + RTL setup with regression tests for both behaviours.
This commit is contained in:
Offending Commit
2026-05-03 16:41:59 -05:00
parent 3fa4d599fe
commit 8f5a6aa7e9
5 changed files with 146 additions and 24 deletions

View File

@@ -1,46 +1,43 @@
import { createRootRoute, Outlet, useRouter } from "@tanstack/react-router";
import { createRootRoute, Outlet, redirect, useRouter } from "@tanstack/react-router";
import { useEffect } from "react";
import { Sidebar } from "@/components/layout/Sidebar";
import { DemoProvider } from "@/context/DemoContext";
import { loadConfig } from "@/lib/config";
import { applyTheme, getStoredTheme } from "@/lib/theme";
const SETTINGS_PATH = "/settings";
function RootLayout() {
const config = loadConfig();
const router = useRouter();
const isSettings = router.state.location.pathname === "/settings";
const isSettings = router.state.location.pathname === SETTINGS_PATH;
useEffect(() => {
applyTheme(getStoredTheme());
}, []);
useEffect(() => {
if (!config && !isSettings) {
router.navigate({ to: "/settings" as never });
}
}, [config, isSettings, router]);
if (isSettings) {
return <Outlet />;
}
if (!config) return null;
return (
<DemoProvider>
<div
className="flex h-screen w-full overflow-hidden"
style={{ background: "var(--bg)", position: "relative", zIndex: 1 }}
>
<Sidebar />
<main className="flex-1 overflow-auto" style={{ position: "relative", zIndex: 1 }}>
<Outlet />
</main>
</div>
</DemoProvider>
<div
className="flex h-screen w-full overflow-hidden"
style={{ background: "var(--bg)", position: "relative", zIndex: 1 }}
>
<Sidebar />
<main className="flex-1 overflow-auto" style={{ position: "relative", zIndex: 1 }}>
<Outlet />
</main>
</div>
);
}
export const Route = createRootRoute({
beforeLoad: ({ location }) => {
// Redirect to settings synchronously when no config is present, so the
// first paint already shows the settings form instead of a blank screen.
if (location.pathname !== SETTINGS_PATH && !loadConfig()) {
throw redirect({ to: SETTINGS_PATH as never });
}
},
component: RootLayout,
});