Problem or Motivation
The Setup Wizard (/setup-wizard) renders in a completely separate full-screen layout that hides the global top navigation bar and ignores the standard page container used by every other page (Radio Presets, Local Control, MultiRoom, Settings, Firmware, etc.).
Current behaviour (problematic)
App.tsx registers /setup-wizard outside the <header>/<main> block that contains <Navigation />. The wizard receives its own <div className="setup-wizard-page-v2"> which fills 100vh independently.
- The
Navigation component (components/Navigation.tsx) is never rendered while the user is in the wizard.
- The wizard cannot be reached from within the app via the nav bar (no nav item), and cannot navigate back to it without the back-button or the URL.
- The
DeviceInfoHeader and ProgressTracker fill the entire viewport width - not the max-width: 1200px page container.
Impact
- Users lose orientation: they have no idea which page they are on in the global context of the app.
- The language selector (
LanguageSelector) is invisible, so language cannot be changed mid-wizard.
- Visually inconsistent: wizard looks like a different application.
- The "Exit wizard" path is unclear (no nav, no breadcrumb).
Desired Behaviour
Layout integration
- The wizard route
/setup-wizard is moved inside the <header>/<main> layout block in AppRouter, so the top <Navigation /> bar is always visible.
- The wizard page content is wrapped in the standard
<div className="page"> container (max-width 1200 px, centred, var(--space-lg) padding) - identical to every other page.
- The
setup-wizard-page-v2 root element is replaced by <div className="page setup-wizard-page"> so it inherits the global layout.
- No new CSS files are introduced; only
SetupWizard.css and App.tsx are changed (and App.css if the transition requires a small layout tweak).
Navigation bar (no structural changes to Navigation)
Navigation.tsx stays unchanged.
- The active route
/setup-wizard gets no nav-link (the wizard is entered via the device card, not directly). The nav bar simply shows as inactive.
- The
LanguageSelector in the nav bar remains fully functional during the wizard.
Page title
- The wizard page gains a standard
<h1 className="page-title"> showing the i18n key setup.wizard.pageTitle (already exists in all four locale files).
- Placed above
DeviceInfoHeader, below the page container opening tag.
Scroll behaviour
- The page scrolls normally inside
<main className="app-main"> (which already has overflow-y: auto). The wizard does not manage its own scroll container.
Empty-state guard
- When no devices are available, the wizard still renders the
wizard-empty-state card inside the page container (unchanged logic, changed wrapper only).
Responsive / Mobile
- Touch targets ? 44 px (WCAG 2.1 AA) - maintained, no regression allowed.
- On screens ? 400 px the wizard content stacks normally, identical to other pages.
Alternatives Considered
| Alternative |
Reason rejected |
| Keep full-screen wizard, add a manual "close" button |
Inconsistent UX; user still loses nav, language selector, and orientation. |
| Add a separate wizard-specific nav bar |
Duplication; increases maintenance burden; violates DRY. |
| Use a modal/overlay for the wizard |
Too disruptive for a multi-step, potentially long workflow; modals are not scrollable on mobile. |
Technical Specification
This section is a complete implementation spec. A /speckit.specify run is not required.
Files to change
| File |
Change |
apps/frontend/src/App.tsx |
Move <Route path="/setup-wizard"> inside the <header>/<main> block |
apps/frontend/src/pages/SetupWizard.tsx |
Replace root element .setup-wizard-page-v2 with .page .setup-wizard-page; add <h1 className="page-title"> |
apps/frontend/src/pages/SetupWizard.css |
Rename/remove .setup-wizard-page-v2 root rule; keep all inner rules |
App.tsx - diff sketch
// BEFORE (line ~103):
<Route path="/setup-wizard" element={<SetupWizard devices={devices} isLoading={false} />} />
// AFTER - moved inside the devices > 0 branch:
<Route
path="/*"
element={
devices.length > 0 ? (
<>
<header className="app-header" data-test="app-header">
<Navigation />
</header>
<main className="app-main">
<Routes>
<Route path="/" element={<RadioPresets devices={devices} />} />
<Route path="/local" element={<LocalControl devices={devices} />} />
<Route path="/multiroom" element={<MultiRoom devices={devices} />} />
<Route path="/firmware" element={<Firmware devices={devices} />} />
<Route path="/settings" element={<Settings />} />
<Route path="/licenses" element={<Licenses />} />
<Route path="/setup-wizard" element={<SetupWizard devices={devices} isLoading={false} />} /> {/* ? NEW POSITION */}
<Route path="*" element={<NotFound />} />
</Routes>
</main>
</>
) : (
<Navigate to="/welcome" replace />
)
}
/>
Guard note: When devices.length === 0 and the user navigates to /setup-wizard, the redirect to /welcome kicks in. This is intentional - the wizard needs at least one device. The existing wizard-empty-state component can be kept as an additional in-wizard guard for direct URL access with a stale device list.
SetupWizard.tsx - root JSX change
// BEFORE:
return (
<div className="setup-wizard-page-v2">
{import.meta.env.DEV && (
<div className="global-demo-banner">.</div>
)}
{selectedDevice && <DeviceInfoHeader device={selectedDevice} />}
<div className="wizard-content-v2">
.
</div>
</div>
);
// AFTER:
return (
<div className="page setup-wizard-page">
{import.meta.env.DEV && (
<div className="global-demo-banner">.</div>
)}
<h1 className="page-title">{t("setup.wizard.pageTitle")}</h1>
{selectedDevice && <DeviceInfoHeader device={selectedDevice} />}
<div className="wizard-content">
.
</div>
</div>
);
SetupWizard.css - root rule rename
/* BEFORE */
.setup-wizard-page-v2 {
min-height: 100vh;
background-color: var(--color-bg-dark);
padding: var(--space-xl) var(--space-md);
}
/* AFTER - remove the root rule entirely; layout is handled by .page in App.css */
/* .setup-wizard-page-v2 is deleted */
/* .setup-wizard-page may carry wizard-specific overrides if needed */
.setup-wizard-page {
/* no min-height: 100vh - scrolls naturally inside app-main */
/* padding comes from .page (var(--space-lg)) */
}
i18n key required
All four locale files (en.json, de.json, fr.json, it.json) already contain:
"setup": {
"wizard": {
"pageTitle": "Setup Wizard" // ? verify this key exists; if missing add it
}
}
If the key is missing in any locale, add it as part of this PR.
Tests to update
| Test file |
Change needed |
apps/frontend/tests/unit/pages/SetupWizard.test.tsx |
Remove any assertion that .setup-wizard-page-v2 exists; assert .page.setup-wizard-page instead |
apps/frontend/tests/e2e/wizard-i18n.cy.ts |
Assert that [data-test="app-header"] is visible while on /setup-wizard (currently it is absent) |
apps/frontend/tests/e2e/wizard-ui-rendering.cy.ts |
Add smoke check: nav links are reachable from wizard URL |
Acceptance Criteria
Additional Context
- Navigation component:
apps/frontend/src/components/Navigation.tsx - no changes required
- Routing architecture:
apps/frontend/src/App.tsx lines 97-135 - wizard route is currently the only top-level route outside the nav wrapper
- CSS variable reference:
var(--space-lg) = 24 px, var(--space-xl) = 32 px (defined in index.css)
- Affected routes:
/setup-wizard only; /welcome and loading/error states are unaffected
- PR scope: Frontend only - no backend changes, no OpenAPI changes, no new dependencies
Problem or Motivation
The Setup Wizard (
/setup-wizard) renders in a completely separate full-screen layout that hides the global top navigation bar and ignores the standard page container used by every other page (Radio Presets, Local Control, MultiRoom, Settings, Firmware, etc.).Current behaviour (problematic)
App.tsxregisters/setup-wizardoutside the<header>/<main>block that contains<Navigation />. The wizard receives its own<div className="setup-wizard-page-v2">which fills100vhindependently.Navigationcomponent (components/Navigation.tsx) is never rendered while the user is in the wizard.DeviceInfoHeaderandProgressTrackerfill the entire viewport width - not themax-width: 1200pxpage container.Impact
LanguageSelector) is invisible, so language cannot be changed mid-wizard.Desired Behaviour
Layout integration
/setup-wizardis moved inside the<header>/<main>layout block inAppRouter, so the top<Navigation />bar is always visible.<div className="page">container (max-width 1200 px, centred,var(--space-lg)padding) - identical to every other page.setup-wizard-page-v2root element is replaced by<div className="page setup-wizard-page">so it inherits the global layout.SetupWizard.cssandApp.tsxare changed (andApp.cssif the transition requires a small layout tweak).Navigation bar (no structural changes to Navigation)
Navigation.tsxstays unchanged./setup-wizardgets no nav-link (the wizard is entered via the device card, not directly). The nav bar simply shows as inactive.LanguageSelectorin the nav bar remains fully functional during the wizard.Page title
<h1 className="page-title">showing the i18n keysetup.wizard.pageTitle(already exists in all four locale files).DeviceInfoHeader, below the page container opening tag.Scroll behaviour
<main className="app-main">(which already hasoverflow-y: auto). The wizard does not manage its own scroll container.Empty-state guard
wizard-empty-statecard inside the page container (unchanged logic, changed wrapper only).Responsive / Mobile
Alternatives Considered
Technical Specification
Files to change
apps/frontend/src/App.tsx<Route path="/setup-wizard">inside the<header>/<main>blockapps/frontend/src/pages/SetupWizard.tsx.setup-wizard-page-v2with.page .setup-wizard-page; add<h1 className="page-title">apps/frontend/src/pages/SetupWizard.css.setup-wizard-page-v2root rule; keep all inner rulesApp.tsx- diff sketchSetupWizard.tsx- root JSX changeSetupWizard.css- root rule renamei18n key required
All four locale files (
en.json,de.json,fr.json,it.json) already contain:If the key is missing in any locale, add it as part of this PR.
Tests to update
apps/frontend/tests/unit/pages/SetupWizard.test.tsx.setup-wizard-page-v2exists; assert.page.setup-wizard-pageinsteadapps/frontend/tests/e2e/wizard-i18n.cy.ts[data-test="app-header"]is visible while on/setup-wizard(currently it is absent)apps/frontend/tests/e2e/wizard-ui-rendering.cy.tsAcceptance Criteria
<header data-test="app-header">is present in the DOM on/setup-wizard<Navigation />(nav links + language selector) is fully functional during all 8 wizard stepsmax-width: 1200px(same as all other pages)app-mainscroll container - no inner scroll container in the wizard<h1 className="page-title">witht("setup.wizard.pageTitle")is rendered aboveDeviceInfoHeaderLanguageSelectorswitches language while the wizard is open (no reload required)DeviceInfoHeaderandProgressTrackerare fully visible and not clippedapps/frontend/tests/unit/pages/SetupWizard.test.tsx- all tests passcd apps/frontend && npx vitest run- 0 failurescd apps/frontend && npx tsc --noEmit- 0 type errorscd apps/frontend && npx eslint src/- 0 lint errorsAdditional Context
apps/frontend/src/components/Navigation.tsx- no changes requiredapps/frontend/src/App.tsxlines 97-135 - wizard route is currently the only top-level route outside the nav wrappervar(--space-lg)= 24 px,var(--space-xl)= 32 px (defined inindex.css)/setup-wizardonly;/welcomeand loading/error states are unaffected