Skip to main content

08 — Runtime Shells Spec

Scope: Each surface mounts a runtime shell that owns chrome (header, nav, footer, modal layer, toast layer, error boundary, telemetry instrumentation, theme provider, locale provider, focus management, idle reset, accessibility scaffolding). This document specifies the four canonical shells.

Companions: 02-architecture-overview-frontend.md · 03-design-system.md · 06-theming-and-tenant-config.md · ../desktop/21-desktop-app-specification.md


Overview — Four canonical shells

ShellUsed byBundle target
Booking shellConsumer Meta Web, Tenant Booking Webapps/web-consumer, apps/web-booking
Operator shellOperator Desktop (Electron), Tablet Front-Deskapps/desktop-operator, apps/tablet-front-desk
Kiosk shellSelf check-in kiosk, Housekeeping kiosk, Arrivals boardapps/kiosk-*
Guest-portal shellGuest Portal Web (/manage), Guest mobile key webviewapps/web-guest-portal

Each shell is a React subtree (not a full Next.js layout) that wraps all pages/screens within that surface. Shells are NOT shared across surfaces — separate bundles minimize cascade risk.


Shell 1: Booking shell

Used by: Consumer Meta Web (melmastoon.com) and Tenant Booking Web ({tenant}.melmastoon.app)

1.1 Layout regions

┌─────────────────────────────────────────────┐
│ [GlobalHeader] │ sticky, z-40
│ logo · search bar · locale picker │
│ currency · shortlist icon · account icon │
├─────────────────────────────────────────────┤
│ [MainContent] │ flex-1, overflow-auto
│ {page children} │
├─────────────────────────────────────────────┤
│ [GlobalFooter] │ static
│ links · locale · currency · legal │
└─────────────────────────────────────────────┘
│ [ModalLayer] │ z-50, portal
│ [DrawerLayer] │ z-45, portal
│ [ToastLayer] │ z-60, portal
│ [BannerLayer] │ below header, z-30
└─────────────────────────────────────────────┘

Tenant Booking variant: GlobalHeader and GlobalFooter are replaced by TenantHeader and TenantFooter — theme-token-aware, populated from theme-config-service. Same layout structure.

1.2 Theme provider integration

Meta web: Default Melmastoon theme from packages/design-tokens.
Tenant booking web: Theme tokens fetched from BFF at request time (SSR), injected via <style id="theme-tokens"> containing CSS custom properties. React ThemeProvider context is populated server-side. On client: React hydration reads tokens from DOM, no flicker.
Switching tenants: Full page navigation; theme is SSR-injected per route. No unmount/remount tricks needed.

1.3 Locale provider integration

LocaleProvider wraps the shell. Locale is resolved at SSR (from cookie mel_locale → Accept-Language header → tenant default → 'en'). dir attribute set on the nearest container (not <html> — responsibility of _document.tsx). Currency resolved from mel_currency cookie.

1.4 Modal / drawer / toast / banner layers

LayerZ-indexManaged byStacking
Banner30BannerStore (Zustand)Below header
Header40Static
Drawer45DrawerStack (Zustand)Right/left side; max 1 open
Modal50ModalStack (Zustand)Center; max 3 stacked
Toast60ToastQueue (Zustand)Top-right; max 5 visible
System prompt (browser)N/ABrowserAbove everything

Scroll lock: Modal open triggers body { overflow: hidden }. Drawer open on mobile also triggers scroll lock. Desktop drawers do NOT lock scroll.

1.5 Focus management on route change

On each Next.js route change (router.events: routeChangeComplete):

  1. window.scrollTo(0, 0)
  2. Focus moves to #page-heading (first <h1> on page) or fallback #main-content skip-link target
  3. Screen reader announcement: route title read via aria-live="assertive" region

1.6 Idle / inactivity behaviour

Not applicable for booking shell (guest-facing; no session timeout UX required). Auth tokens have a rolling refresh via BFF; silent re-auth on 401.

1.7 Telemetry instrumentation

On route change: page_view event emitted with { page_type, tenant_id, locale, currency, is_rtl, is_pwa }.
Shell mounts PerformanceObserver for LCP, FID, INP, CLS — forwarded to @ghasi/telemetry → RUM pipeline.
traceparent propagated from SSR response header into page <meta name="traceparent"> and picked up by shell's telemetry init.

1.8 Auth boundary

Meta web: Unauthenticated by default. Auth gate triggered only on shortlist save (→ requires account) or "Manage bookings" (→ requires account).
Tenant booking web: Unauthenticated for browse; auth gate at payment step (optional — guest checkout also allowed).
Auth gate: Shell renders AuthModal (magic link or Google SSO). On successful auth, shell re-fetches BFF session cookie and resumes the interrupted action.

1.9 Error boundary

Top-level React Error Boundary in the shell catches unhandled render errors:

  • Logs to Sentry with correlation-id from @ghasi/telemetry
  • Renders GlobalErrorPage component (full-page graceful degradation: "Something went wrong; we've been notified. [Reload page]")
  • Preserves URL so user can reload or navigate back

Nested error boundaries within page components catch section-level errors (e.g., a RoomGrid render failure shows an inline error card, not a full-page error).

1.10 Performance: shell budgets

AssetBudget
Shell JS (booking shell)≤ 25 kB gzipped
Shell CSS≤ 8 kB gzipped
Shell render time (SSR)≤ 80 ms (p95)
First Contentful Paint (shell chrome)≤ 1.0 s

Shell provides:

  • <a href="#main-content" class="sr-only focus:not-sr-only">Skip to main content</a> at top of <body>
  • <header role="banner"> (GlobalHeader)
  • <main id="main-content" role="main"> (MainContent)
  • <footer role="contentinfo"> (GlobalFooter)
  • <nav aria-label="Main navigation"> within GlobalHeader
  • Live region <div aria-live="polite" aria-atomic="true" id="route-announcer"> for SPA navigation announcements

1.12 RTL behaviour

dir="rtl" on <html> (set by _document.tsx from locale). Shell regions use CSS logical properties. No position: left/right in shell chrome — use inset-inline-start/end. Toast position: top-inline-end (top-right in LTR, top-left in RTL).


Shell 2: Operator shell

Used by: Operator Desktop (Electron), Tablet Front-Desk

2.1 Layout regions

┌────┬────────────────────────────────────────┐
│ │ [TopBar] │ h-14, z-40
│ │ breadcrumb · search · alerts · user │
│ S ├────────────────────────────────────────┤
│ i │ │
│ d │ [MainContent] │
│ e │ {page children} │
│ b │ │
│ a ├────────────────────────────────────────┤
│ r │ [StatusBar] (Desktop only) │ h-6, z-10
└────┴────────────────────────────────────────┘
│ [NotificationPanel] (slide-over) │ z-45, right
│ [ModalLayer] │ z-50
│ [ToastLayer] │ z-60, bottom-right
└─────────────────────────────────────────────┘

Sidebar: 64px icon-only (collapsed) ↔ 240px labelled (expanded). Collapsed by default on tablet; always-expanded on desktop (user preference stored in localStorage).

Sidebar sections:

  • Dashboard
  • Reservations (arrivals / departures / in-house)
  • Housekeeping board
  • Folios
  • Guests
  • Rooms
  • Reports
  • Settings

2.2 Theme provider integration

Operator shell uses the operator-theme token set (neutral palette, high information density). Multi-tenant context: operator's own hotel brand color appears in the sidebar accent and top-bar. Theme tokens loaded from theme-config-service on login, cached in Zustand themeStore.

Electron: Theme tokens loaded from SQLite cache (updated on sync). Dark mode: system preference + operator preference toggle. prefers-color-scheme media query updates data-color-scheme attribute on shell container.

2.3 Locale provider integration

Locale and RTL state set from operator profile (operator.locale, operator.rtl). On Electron, from SQLite prefs. On Tablet, from BFF session. Locale switcher available in Settings only (not top-bar) — operators don't switch locale mid-session.

2.4 Modal / drawer / toast / banner layers

LayerZ-indexStacking
Notification panel (slide-over)45Right edge; does not lock scroll
Modal50Center; max 3 stacked (e.g., confirm → reason → receipt)
Toast60Bottom-right (desktop) / bottom-center (tablet)

Toast on operator shell: Auto-dismiss 5 s for informational; persistent (requires acknowledgement) for errors. Max 5 visible; oldest dismissed when limit reached.

2.5 Focus management on route change

  • Route change focuses first interactive element in <main> (or explicit data-autofocus target)
  • No scroll-to-top (operator may navigate back and forth; maintain scroll position per route via scroll-restoration store)
  • Breadcrumb updated immediately; aria-current="page" on active breadcrumb segment

2.6 Idle / inactivity behaviour

Desktop: After 30 min of inactivity (no mouse/keyboard input): lock screen (full-screen overlay requiring biometric/PIN re-auth). Configurable per tenant (15–60 min). Events that reset timer: mouse move, keydown, touchstart.

Tablet: After 15 min: dimmed overlay + "Tap to continue". After 30 min: full re-auth required.

Kiosk variant: Separate — see Shell 3.

2.7 Telemetry instrumentation

screen_view event on route change: { screen_name, tenant_id, operator_id_hashed, surface: 'operator' }.
Performance: Time-to-interactive per key screen (dashboard load, reservation list load) tracked as custom metric.

2.8 Auth boundary

Operator shell is fully authenticated. On 401/403 from BFF:

  • 401 (token expired): silent re-auth via refresh token. If refresh fails: redirect to /login.
  • 403 (insufficient permission): render UnauthorizedPage within <main> without full redirect.

2.9 Error boundary

Same pattern as booking shell. Operator shell additionally:

  • Sends error telemetry with operator_id_hashed and screen_name
  • Preserves sidebar navigation (error boundary wraps only <main>, not sidebar)
  • Provides "Report an issue" shortcut in the error page (opens pre-filled support ticket)

2.10 Performance: shell budgets

AssetBudget
Shell JS (operator shell)≤ 40 kB gzipped
Shell CSS≤ 10 kB gzipped
Sidebar initial render≤ 100 ms
Desktop: cold start to interactive≤ 4 s (see 09-NFR.md §1.2)

2.11 Accessibility

  • Skip link: "Skip to main content"
  • Sidebar: <nav aria-label="Primary navigation">; active item aria-current="page"
  • Keyboard shortcut Ctrl+K / Cmd+K opens global search (Command Palette pattern)
  • All modals: focus trap; Escape closes; focus returns to trigger element on close
  • Toast: role="status" (informational) or role="alert" (error/warning)

2.12 RTL

Sidebar flips to right side in RTL. TopBar breadcrumb reads right-to-left. All other logical-property rules apply.


Shell 3: Kiosk shell

Used by: Self check-in kiosk, Housekeeping kiosk, Arrivals board / TV mode

3.1 Layout regions

┌─────────────────────────────────────────────┐
│ [KioskHeader] │ h-20; tenant logo + clock + language
├─────────────────────────────────────────────┤
│ │
│ [MainContent] │ flex-1; centered; max-w-xl
│ {kiosk screen} │
│ │
├─────────────────────────────────────────────┤
│ [KioskFooter] │ h-16; "Need help?" + accessibility btn
└─────────────────────────────────────────────┘
│ [ModalLayer] │ z-50; large tap targets
│ [AlertLayer] │ z-60; full-screen overlays
└─────────────────────────────────────────────┘

Touch targets: All interactive elements ≥ 60×60 px (SC 2.5.5 enhanced; WCAG 2.2 minimum is 24×24 px — kiosk exceeds this for accessibility and gloved-hand use).

Attract loop: When idle for 60 s, the AttractLoop component takes over: branded slideshow (property photos + "Check in" CTA). Tap anywhere resumes session.

3.2 Theme provider integration

Same as operator shell: tenant tokens from theme-config-service. The kiosk is typically on a dedicated device per property; tokens loaded on app start and cached in IndexedDB.

3.3 Locale provider integration

Kiosk shows a language picker on the landing screen (large flag/script buttons: عربي | پښتو | دری | English | Français). Locale selection stored in sessionStorage (cleared between sessions). dir applied to <html> dynamically.

3.4 Modal / alert layers

  • Modal: Full-screen size on kiosk (not centered dialog — the full screen IS the modal content). Back button in modal header.
  • Alert: Critical system alerts (printer offline, payment terminal unreachable) render as persistent full-screen overlays until resolved.
  • No toasts: Toast pattern is not used on kiosk — all feedback is inline or via full-screen overlays.

3.5 Focus management on route change

Kiosk is primarily touch-driven. On screen change:

  • Focus placed on first focusable element (usually the primary CTA button)
  • For accessibility kiosk mode (screen-reader + switch access): full keyboard navigation enabled; route changes trigger TTS announcement via Web Speech API

3.6 Idle / inactivity behaviour

Attract loop: Idle > 60 s → attract loop
Session reset: Idle > 120 s during an active session → warning modal "Are you still there?" (15 s countdown) → full session reset (clear all form data, return to welcome screen)
Staff override: Hidden gesture (3-tap on kiosk logo, 2 s hold) → staff PIN entry → backdoor to housekeeping or settings mode

What survives reset:

  • Locale preference: YES (sticky per device)
  • Partially filled booking: NO (cleared)
  • Payment terminal state: terminal self-manages (not reset by app)

3.7 Telemetry

kiosk_screen_view, kiosk_session_start, kiosk_session_complete, kiosk_session_abandoned (with stage at abandonment). No PII in telemetry (guest name/email/passport never logged).

3.8 Auth boundary

Guest kiosk: No guest login — authentication is via booking reference + last name (or QR code from email). Kiosk backend validates through check-in-service.
Staff kiosk (housekeeping): Staff PIN → role-scoped session. BFF validates PIN against identity-service.

3.9 Error boundary

Unhandled errors on kiosk:

  • Log to Sentry
  • Display full-screen "Oops! Please see a staff member." message with staff-facing error code
  • Auto-restart app after 30 s (Electron app.relaunch() or browser location.reload())

3.10 Performance

MetricTarget
Welcome screen interactive≤ 2 s cold start
Screen transition animation300 ms (CSS transition)
Payment terminal poll latency≤ 200 ms

3.11 Accessibility

  • Low-literacy mode: Icon-first labels; simple language; optional TTS readout via Web Speech API
  • Wheelchair height: Kiosk UI constrained to upper 60% of screen (lower 40% is blank) for countertop kiosk height variants
  • Colour blind safe: All status indicators use both colour and icon/label
  • Switch access: All interactions achievable with a single switch (scan mode navigation)

3.12 RTL

Applied dynamically based on language picker selection. Full layout mirror. Test RTL in Playwright for every kiosk screen.


Shell 4: Guest-portal shell

Used by: Guest Portal Web (/manage), Guest Mobile Key webview

4.1 Layout regions

┌─────────────────────────────────────────────┐
│ [GuestPortalHeader] │ sticky, z-40
│ property logo · "Hi, [FirstName]" · menu │
├─────────────────────────────────────────────┤
│ [TabBar] (mobile) or [SideNav] (desktop) │
│ Bookings · Requests · Messages · Receipts │
├─────────────────────────────────────────────┤
│ [MainContent] │ flex-1
│ {page children} │
└─────────────────────────────────────────────┘
│ [ModalLayer] │ z-50
│ [ToastLayer] │ z-60, bottom-center on mobile
└─────────────────────────────────────────────┘

Mobile-first: TabBar replaces SideNav on mobile. Bottom tab bar pattern (5-tab max; overflow → "More" tab).

4.2 Theme provider integration

Guest portal is tenant-branded (same tenant the guest booked with). Theme tokens injected via SSR (same mechanism as tenant booking web). The shell's header shows the property's logo (from theme-config-service).

Guest key webview: Stripped shell — no header, no nav. Just the key card screen wrapped in the theme tokens.

4.3 Locale provider integration

Locale from guest's booking locale (stored in reservation.locale). Guest can override via profile settings. RTL applied accordingly.

4.4 Layers

Same z-index policy as booking shell. Toast on mobile appears at bottom-center (above tab bar). On desktop, top-right.

4.5 Focus management

Tab bar changes: focus moves to heading of new section. Full keyboard nav in desktop SideNav.

4.6 Idle / inactivity behaviour

Magic-link sessions expire after 7 days of inactivity. On expiry:

  • BFF returns 401
  • Shell shows "Your session has expired" full-screen overlay with "Get a new link" CTA (sends a new magic link to the guest's email)
  • No mid-session idle warning (guest portal is used briefly, not for extended sessions)

4.7 Telemetry

portal_screen_view with { screen, reservation_id_hashed, tenant_id, locale }.

4.8 Auth boundary

All routes in guest portal require auth (magic link session cookie). Unauthenticated users are redirected to /manage/login where they enter their email to receive a magic link. No password, no social login (by design — simplicity for guests).

4.9 Error boundary

Same pattern as booking shell. Error page includes "Contact the hotel" CTA (opens ContactForm or messaging thread).

4.10 Performance

MetricTarget
Portal home interactive≤ 2.5 s (LCP)
Guest key screen interactive≤ 1.5 s

4.11 Accessibility

Same skip links and landmark pattern as booking shell. Tab bar: role="tablist" with role="tab" per item and role="tabpanel" on main content.

4.12 RTL

Same as booking shell. Tab bar labels and icons mirror in RTL. Back/forward navigation arrows flip.


Cross-shell conventions

Shell registry

Each shell registers itself at mount time with @ghasi/shell-registry:

shellRegistry.register({
id: 'booking',
version: '1.0.0',
surface: 'web-consumer' | 'web-booking',
telemetry: { prefix: 'booking' },
});

This allows platform tools (devtools panel, telemetry dashboard) to identify which shell is active.

Error correlation

Every shell injects correlation-id (from BFF response header or generated client-side) into:

  • Sentry breadcrumb context
  • All subsequent fetch requests (X-Correlation-Id header)
  • Telemetry events

Shell versioning

Shells are independently versioned. Breaking changes to a shell (e.g., removing a landmark region) require an ADR and a migration note in this document.


Open Questions

  • Operator shell and tablet-front-desk shell: same shell with density mode swap, or distinct shells? Current decision: same shell with data-density="compact" attribute on tablet. Revisit if tablet layout diverges significantly.
  • Kiosk shell: hidden staff override gesture — is a 3-tap gesture secure enough or should we require a QR code scan?
  • Idle reset on kiosk: does locale preference survive reset? Current decision: YES (device-level preference).
  • Guest-portal shell: should desktop layout use SideNav or tabs? Current: SideNav on desktop, TabBar on mobile.

References