Files
flights_web/docs/superpowers/plans/2026-04-14-phase-1f-layout.md
T

7.5 KiB

Phase 1F-layout — Root layout + routes + error mapper contracts

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Ship the root layout provider stack, locale-scoped layout, error boundary, error-to-HTTP mapper, error pages, and smoke route — so that all downstream feature routes render inside a fully-wired provider tree (LoggerProvider > ApiClientProvider > I18nProvider > ErrorBoundary) and error handling works end-to-end.

Architecture: Modern.js file-based routing. src/routes/layout.tsx wraps children with the global providers (Logger, ApiClient, ErrorBoundary). src/routes/[lang]/layout.tsx validates the lang param, creates a request-scoped i18n instance, and wraps children with <I18nProvider>. Error pages live at src/routes/error/[code]/page.tsx. The smoke route at src/routes/[lang]/smoke/page.tsx exercises logger, i18n, and locale display.

Tech Stack: React 18, Modern.js SSR, i18next, Vitest.

Prerequisites: 1A-1 (skeleton), 1A-2 (MF builds), 1C (i18n), 1D (API client), 1G-logger (logger).


File structure

File Responsibility Task
src/ui/errors/ErrorBoundary.tsx React error boundary with retry 1
src/routes/error/map.ts errorToResponse() mapper 2
src/routes/error/map.test.ts TDD tests for mapper 2
src/routes/layout.tsx Root layout with provider stack 3
src/routes/[lang]/layout.tsx Locale-scoped layout 3
src/routes/error/[code]/page.tsx Error page (404, 500, 503) 4
src/i18n/locales/en/common.json Add SMOKE keys 5
src/i18n/locales/ru/common.json Add SMOKE keys 5
src/routes/[lang]/smoke/page.tsx Smoke route 5

Task 1 — ErrorBoundary component

Files:

  • Create: src/ui/errors/ErrorBoundary.tsx

  • Step 1: Create the ErrorBoundary class component

The ErrorBoundary must be a class component (React requirement for componentDidCatch). It catches errors in its subtree, renders a fallback UI with a "Retry" button that resets the boundary state.

// src/ui/errors/ErrorBoundary.tsx
import { Component } from "react";
import type { ReactNode, ErrorInfo } from "react";

interface ErrorBoundaryProps {
  children: ReactNode;
  fallback?: ReactNode;
}

interface ErrorBoundaryState {
  hasError: boolean;
  error: Error | null;
}

export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
  state: ErrorBoundaryState = { hasError: false, error: null };

  static getDerivedStateFromError(error: Error): ErrorBoundaryState {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, info: ErrorInfo): void {
    console.error("[ErrorBoundary]", error, info.componentStack);
  }

  handleRetry = (): void => {
    this.setState({ hasError: false, error: null });
  };

  render(): ReactNode {
    if (this.state.hasError) {
      if (this.props.fallback) return this.props.fallback;
      return (
        <div role="alert">
          <h2>Something went wrong</h2>
          <p>{this.state.error?.message}</p>
          <button type="button" onClick={this.handleRetry}>Retry</button>
        </div>
      );
    }
    return this.props.children;
  }
}
  • Step 2: Verify typecheck
pnpm typecheck
  • Step 3: Commit
git add src/ui/errors/ErrorBoundary.tsx
git commit -m "Add ErrorBoundary class component with retry support"

Task 2 — TDD errorToResponse mapper

Files:

  • Create: src/routes/error/map.ts

  • Create: src/routes/error/map.test.ts

  • Step 1: Write failing tests

Tests cover all four mapping rules:

  1. ApiHttpError(404) -> { status: 404, errorCode: "not_found" }
  2. ApiHttpError(502) -> { status: 500, errorCode: "internal" }
  3. ApiTimeoutError -> { status: 503, headers: { "Retry-After": "30" }, errorCode: "unavailable" }
  4. Unknown error -> { status: 500, errorCode: "internal" }
  • Step 2: Write the implementation
export interface ErrorResponse {
  status: 404 | 500 | 503;
  headers?: Record<string, string>;
  errorCode: "not_found" | "internal" | "unavailable";
}

export function errorToResponse(error: unknown): ErrorResponse;
  • Step 3: Run tests — all must pass
pnpm test -- src/routes/error/map.test.ts
  • Step 4: Commit
git add src/routes/error/map.ts src/routes/error/map.test.ts
git commit -m "Add errorToResponse mapper with TDD tests"

Task 3 — Root layout + locale-scoped layout

Files:

  • Modify: src/routes/layout.tsx (replace 1A-2 stub)

  • Create: src/routes/[lang]/layout.tsx

  • Step 1: Update root layout

Replace the stub with the real provider stack:

  • <LoggerProvider> wrapping everything (logger from createRootLogger())

  • <ApiClientProvider> with a default-locale ApiClient

  • <ErrorBoundary> wrapping children

  • Step 2: Create locale-scoped layout

src/routes/[lang]/layout.tsx:

  • Validate params.lang using isLanguage() from @/i18n/resolver

  • If invalid lang, redirect to /ru/ (or render 404)

  • Create i18n instance via createI18nInstance({ locale: params.lang })

  • Wrap children with <I18nProvider>

  • Step 3: Verify typecheck

pnpm typecheck
  • Step 4: Commit
git add src/routes/layout.tsx src/routes/\[lang\]/layout.tsx
git commit -m "Wire root layout provider stack and locale-scoped layout"

Task 4 — Error pages

Files:

  • Create: src/routes/error/[code]/page.tsx

  • Step 1: Create error page component

Simple text-based UI for codes 404, 500, 503. Renders heading, description, and a link back to home. No design system dependency.

  • Step 2: Verify typecheck
pnpm typecheck
  • Step 3: Commit
git add src/routes/error/\[code\]/page.tsx
git commit -m "Add error pages for 404, 500, 503 codes"

Task 5 — Smoke route + i18n keys

Files:

  • Modify: src/i18n/locales/en/common.json (add SMOKE keys)

  • Modify: src/i18n/locales/ru/common.json (add SMOKE keys)

  • Create: src/routes/[lang]/smoke/page.tsx

  • Step 1: Add SMOKE i18n keys

Add to en/common.json:

"SMOKE": {
  "HEADING": "Smoke test page"
}

Add to ru/common.json:

"SMOKE": {
  "HEADING": "Страница проверки"
}
  • Step 2: Create smoke page

src/routes/[lang]/smoke/page.tsx:

  • Uses useTranslation() to render t("SMOKE.HEADING")

  • Uses useLogger() to emit an info log on mount (via useEffect)

  • Displays the current locale from the URL params

  • Step 3: Verify typecheck + lint + test

pnpm typecheck && pnpm lint && pnpm test
  • Step 4: Build
pnpm build:standalone
  • Step 5: Commit
git add src/i18n/locales/en/common.json src/i18n/locales/ru/common.json src/routes/\[lang\]/smoke/page.tsx
git commit -m "Add smoke route exercising logger, i18n, and locale display"

Exit gate

  • pnpm typecheck && pnpm lint && pnpm test — all pass
  • pnpm build:standalone — succeeds
  • src/routes/[lang]/smoke/page.tsx exists
  • src/ui/errors/ErrorBoundary.tsx exists
  • src/routes/error/map.ts exists with errorToResponse() exported
  • src/routes/error/[code]/page.tsx exists
  • src/routes/layout.tsx wraps children with LoggerProvider, ApiClientProvider, ErrorBoundary
  • src/routes/[lang]/layout.tsx validates lang and provides I18nProvider