PVR Tech Studio
Page layouts previews

Page-Layout Previews

The 20 try-before-you-apply preview routes under /page-layouts/.

9 min read
Updated July 15, 2026

Overview

The Page Layouts menu group (Dashboard → Page Layouts) is a gallery of demonstration pages. Each route shows exactly one shell option — a sidebar mode, a sidebar theme, a navigation behavior, a widget, or a feedback/motion setting — applied to a believable, skeleton-shaped dashboard. Opening a preview temporarily patches the layout config so you experience the option in context; leaving the /page-layouts/* area restores whatever you actually had configured. It is the safest way to shop for a look before making it permanent.

There is no bespoke component per route. A single templateLayoutPreviewPage({variant}) — is parameterized by a variant string and reads its definition from the LAYOUT_PREVIEWS array. That array is also folded into the app's route registry, so adding an entry creates a real, code-split route automatically. This is the same "data drives the routes" idiom used elsewhere in the template.

Architecture & files

FileResponsibility
src/pages/layouts/LayoutPreviewPage.tsxThe shared template, the LAYOUT_PREVIEWS data array, GROUP_ORDER, the PreviewSkeleton demo content, previewSession, and usePreviewSessionGuard.
src/routes.tsxFolds LAYOUT_PREVIEWS into appRoutes (each becomes a real route). This file is eager, not lazy, because the array is read at module-eval to build the routes.
src/data/menu.tsThe pageLayouts menu group + its five nested subgroups (one nav leaf per preview).
src/layout/AppLayout.tsxCalls usePreviewSessionGuard() on the stable shell so the captured baseline is restored on exit.
src/context/LayoutContext.tsxOwns the LayoutConfig each preview patches (see Layout System).
src/locales/en/pages.jsonThe lp*Desc / lp*Hint copy shown on each preview (keys listed below).

Usage

Reach the previews from the sidebar (Dashboard → Page Layouts), the ⌘K palette, or by navigating to a /page-layouts/<variant> route directly. As a developer you never render LayoutPreviewPage by hand — you add a row to LAYOUT_PREVIEWS and the route + menu leaf appear.

// src/routes.tsx already does this — LAYOUT_PREVIEWS becomes real routes:
import {LAYOUT_PREVIEWS, LayoutPreviewPage} from '@/pages/layouts/LayoutPreviewPage'
 
const previewRoutes = LAYOUT_PREVIEWS.map((p) => ({
    path: p.to,
    element: <LayoutPreviewPage variant={p.variant} />,
}))

API / Props

PreviewDef shape

Each entry in LAYOUT_PREVIEWS is a plain object:

FieldTypeDescription
variantLayoutPreviewVariantUnique id; also the LayoutPreviewPage prop and the trailing URL segment.
groupPreviewGroupOne of modes / theme / navigation / widgets / feedback (drives grouping + nav).
tostringThe route path, e.g. /page-layouts/minified.
navKeystringnav: i18n key for the sidebar label.
descKeystringpages: i18n key for the preview's description paragraph.
hintKeystringpages: i18n key for the "try this" hint.
iconLucideIconIcon shown on the preview header.
configPartial<LayoutConfig>The layout patch applied on mount (against the captured baseline).
themeMode?ThemeModeForce a theme mode while previewing (sidebar-theme previews force 'light').
live?{type: 'options', field, opts} | {type: 'toggle', field}Optional interactive controls rendered on the page (see below).

The five groups

Groups (and their sidebar order) come from GROUP_ORDER:

GroupSidebar heading (nav: key)Previews
modesplSidebarModesDefault, Minified, Dual, Header Only
themeplSidebarThemeSidebar Light, Sidebar Dark, Sidebar True Black
navigationplNavigationFixed Navbar, Auto-hide Navbar, Sticky Page Header, Primary Navigation, Accordion Menu, Fixed Footer
widgetsplWidgetsChat Bubble, Chat Rail, Cursor Glow
feedbackplFeedbackToast Position, Page Transitions, Page Loader, Splash Frequency

Every preview route

The LayoutConfig column is the exact config patch applied on mount; blank means the preview applies no persistent patch and is driven purely by its live control. Descriptions are the lp*Desc strings from src/locales/en/pages.json.

Sidebar Modes (group: 'modes')

RouteWhat it demonstratesLayoutConfig patch
/page-layouts/defaultThe standard shell: full sidebar left, header on top, content in the main column — the baseline every other preview builds on.minSidebar:false, dualSidebar:false, headerOnly:false
/page-layouts/minifiedSidebar collapses to a slim icon rail that expands on hover, giving content more room.minSidebar:true, dualSidebar:false, headerOnly:false
/page-layouts/dualA narrow icon rail of menu sections beside a secondary panel showing the active section.dualSidebar:true, minSidebar:false, headerOnly:false
/page-layouts/header-onlyThe sidebar is hidden entirely; navigation moves into the top header.headerOnly:true, minSidebar:false, dualSidebar:false

Sidebar Theme (group: 'theme' — all force themeMode: 'light' so the sidebar treatment is visible)

RouteWhat it demonstratesLayoutConfig patch
/page-layouts/sidebar-lightA light sidebar matching a light page theme — clean and airy.darkSidebar:false, oledSidebar:false
/page-layouts/sidebar-darkA dark sidebar on a light page — the popular high-contrast admin look.darkSidebar:true, oledSidebar:false
/page-layouts/sidebar-blackA true-black (OLED) sidebar — pure-black neutrals for maximum contrast / OLED battery savings.darkSidebar:true, oledSidebar:true

Navigation (group: 'navigation')

RouteWhat it demonstratesLayoutConfig patch
/page-layouts/fixed-navbarThe header stays pinned to the top while page content scrolls beneath it.fixedNav:true, autoHideNav:false
/page-layouts/auto-hide-navbarThe header hides on scroll-down and reappears on scroll-up.autoHideNav:true, fixedNav:true, stickyPageHeader:false
/page-layouts/sticky-headerThe slim page header sticks just below the top navigation as you scroll.stickyPageHeader:true, autoHideNav:false
/page-layouts/primary-navShows the primary navigation links (Home, mega-menus) in the top header. Live toggle.showPrimaryNav:true
/page-layouts/accordion-menuSidebar groups behave as an accordion — opening one closes the others. Live toggle.accordionMenu:true
/page-layouts/fixed-footerThe footer is pinned to the bottom of the viewport instead of sitting at page end.fixedFooter:true

Widgets (group: 'widgets')

RouteWhat it demonstratesLayoutConfig patch
/page-layouts/chat-bubbleA floating chat launcher pinned bottom-right, opening a messenger panel.chatBubble:true, chatRail:false
/page-layouts/chat-railA slim right-pinned rail of contacts with pop-out chat windows.chatRail:true, chatBubble:false
/page-layouts/cursor-glowAn ambient radial glow that softly follows the cursor (desktop only).cursorGlow:true

Feedback & Motion (group: 'feedback' — each has interactive live controls; no persistent patch)

RouteWhat it demonstratesLive control
/page-layouts/toast-positionChoose where toast notifications appear; click a position to fire a demo toast there.options → toastPosition
/page-layouts/page-transitionChoose the animation played when navigating between pages.options → pageTransition
/page-layouts/page-loaderChoose the splash / route-loader style; clicking a style replays the full-screen splash.options → pageLoader
/page-layouts/splash-frequencyChoose how often the branded splash appears (once per session vs every load).options → splashFrequency

Configuration & customization

How a preview applies & reverts

  • Apply on mount, against a captured baseline. Each preview patches the layout as {...baseline, ...preview.config} (plus preview.themeMode when set), so it always shows a clean state — no leftover flags from the previously-viewed preview.
  • Revert is owned by a module-level previewSession plus usePreviewSessionGuard(), which is called in AppLayout (the stable component). The routed subtree remounts on every navigation via the keyed PageTransition, so the baseline is captured once on first entry into /page-layouts/* and restored when you navigate away from the group.
  • One entry toast. A single informational toast fires on entry to the preview area (guarded by the session), so hopping between previews never stacks toasts.

Live controls (live)

Some previews render interactive controls that apply their value live instead of (or in addition to) a static config patch:

  • {type: 'toggle', field} — an on/off switch bound to a boolean LayoutConfig key. Used by Primary Navigation (showPrimaryNav) and Accordion Menu (accordionMenu).
  • {type: 'options', field, opts} — a labelled pill group (reusing the customizer: option labels). Used by Toast Position (toastPosition), Page Transitions (pageTransition), Page Loader (pageLoader), and Splash Frequency (splashFrequency). Clicking a toast-position pill fires a demo toast; clicking a page-loader pill replays the splash by dispatching the window event app:replay-splash (which AppSplash listens for).

Adding a preview

Add one object to LAYOUT_PREVIEWS in src/pages/layouts/LayoutPreviewPage.tsx:

{
    variant: 'my-option',
    group: 'navigation',
    to: '/page-layouts/my-option',
    navKey: 'layoutMyOption',        // add nav:layoutMyOption
    descKey: 'lpMyOptionDesc',       // add pages:lpMyOptionDesc
    hintKey: 'lpMyOptionHint',       // add pages:lpMyOptionHint
    icon: SomeIcon,
    config: {myFlag: true},          // a Partial<LayoutConfig>
    // live: {type: 'toggle', field: 'myFlag'},  // optional interactive control
}

The route (routes.tsx) and the sidebar leaf (via menu.ts) follow automatically once the i18n keys exist. Add the matching LayoutConfig key first (see Layout System).

Examples

Force a specific theme while previewing (the sidebar-theme previews do this so the dark/OLED sidebar is actually visible on a light page):

{
    variant: 'sidebar-black',
    group: 'theme',
    to: '/page-layouts/sidebar-black',
    navKey: 'layoutSidebarBlack',
    descKey: 'lpSidebarBlackDesc',
    hintKey: 'lpSidebarBlackHint',
    icon: MoonStar,
    themeMode: 'light',                       // show it against a light page
    config: {darkSidebar: true, oledSidebar: true},
}

Best practices

  • Previews are presentation-only. They never write the persisted customizer config — that's why leaving the area cleanly reverts. Don't add a preview that permanently mutates localStorage.
  • Patch against the baseline, not the live config. Reuse the {...baseline, ...config} pattern so a preview never inherits stray flags from a sibling.
  • Reuse the shared option tables (customizerOptions.ts) for live option pills so labels match the Customizer and stay i18n-driven.
  • Keep the demo content skeletal. PreviewSkeleton is intentionally shimmer-shaped dashboard content (with a ScrambleText "demo content" caption) — not a loading state and not real data.

Troubleshooting

SymptomLikely causeFix
Your real layout changed after visiting previewsThe revert guard didn't runEnsure usePreviewSessionGuard() stays called in AppLayout (the stable shell), not in a routed child
A preview shows leftover flags from the last onePatch applied against live config, not the baselineApply {...baseline, ...preview.config}
The entry toast fires repeatedlyToast not gated by the preview sessionKeep the single-toast guard on previewSession
A new preview has no route/menu leafMissing i18n keys or not in LAYOUT_PREVIEWSAdd the nav:/pages: keys and the array entry
Dark/OLED sidebar preview looks like normal dark modethemeMode not forced to 'light'Set themeMode: 'light' on sidebar-theme previews

FAQ

Do previews change my saved settings? No. They apply temporarily and revert when you leave the /page-layouts/* area. Make a look permanent in the Customizer or Layout Settings.

Why are these routes eager (not React.lazy)? routes.tsx reads LAYOUT_PREVIEWS at module-eval to build appRoutes, so the module can't be lazily imported. It's the one page module (besides errors/*) that stays eager.

Where does the preview copy live? In the pages i18n namespace — the lp*Desc / lp*Hint keys in src/locales/<lng>/pages.json. Labels are the nav: and customizer: keys.

Can I preview two options at once? Not from the gallery — each route demonstrates one option. Combine options in the Customizer, which previews everything together live.

Notes for designers & content editors

  • Copy (each preview's description + hint) is i18n text — edit lp*Desc / lp*Hint in src/locales/<lng>/pages.json, not the component.
  • The demo content is deliberately abstract (token-based Skeleton shimmer) so the layout reads, not fake data — keep it that way when tweaking.
  • Colors come from tokens; the previews inherit the active skin/theme automatically.

Was this page helpful?