Skip to content

[Feature]: Wizard runs inside standard layout with top navigation bar visible #113

@scheilch

Description

@scheilch

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

  1. The wizard route /setup-wizard is moved inside the <header>/<main> layout block in AppRouter, so the top <Navigation /> bar is always visible.
  2. 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.
  3. The setup-wizard-page-v2 root element is replaced by <div className="page setup-wizard-page"> so it inherits the global layout.
  4. 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

  • <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 steps
  • The wizard content is constrained to max-width: 1200px (same as all other pages)
  • Scrolling works via the global app-main scroll container - no inner scroll container in the wizard
  • <h1 className="page-title"> with t("setup.wizard.pageTitle") is rendered above DeviceInfoHeader
  • LanguageSelector switches language while the wizard is open (no reload required)
  • DeviceInfoHeader and ProgressTracker are fully visible and not clipped
  • No visual regression on ? 1200 px desktop and ? 400 px mobile viewports
  • apps/frontend/tests/unit/pages/SetupWizard.test.tsx - all tests pass
  • cd apps/frontend && npx vitest run - 0 failures
  • cd apps/frontend && npx tsc --noEmit - 0 type errors
  • cd apps/frontend && npx eslint src/ - 0 lint errors

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions