Skip to main content
Brand
Changelogv0.45
Home/Components · Review tooling

Review Mode

Turn a running React app into its own design review workbench. Mount it once behind a build flag; production never ships it.

Pre-release
Updated Oct 19 2026
brandOS v0.45.0

One mount · no second screen list · nothing in production

Review the real app, not a copy of it

Review Mode reads the app’s own route tree, so the screen and navigation maps never drift from what is built. Reviewers force states, compare proposed designs, pin notes and run a self-check on the screens people will use.

  • A manifest
  • One lazy mount
  • Design routes
  • Forced state at the data seam

01 · What it adds

Everything a review needs, on the app itself

Review bar

Areas, structure pages, Built and Proposed variants, screen picker, forced states, roles, width presets, light and dark.

Forced states

Loaded, empty, loading, error and no access, driven from the address so every reviewer sees the same thing.

Pinned notes

Notes pinned to elements on the screen, with a backlog page that tracks open, doing, done and won’t do.

Self-check

Structure, overflow, contrast, breakpoints, tokens, placeholders and console errors, for one page or every page.

Design pages

Screen map and navigation map derived from the route tree, plus design system, design ideas and brainstorm.

Production-safe

Mounted behind a build flag. Screens import only inert hooks, so production builds carry none of it.

02 · Try it

Live demo

The demo is a small sample app with the real bar mounted over it. Switch Orders to Empty or Error, flip it to Proposed, press N to see the pinned note, try the 390 width preset and open the screen map from the Structure row.

Open the live demo

03 · Install

Add the package

pnpm add -D @snyder-tech/brandos-review-mode

Peer dependencies are React 19 and React DOM 19. TanStack Router is needed only for the bundled adapter, TanStack Query only forwithReviewQueryClient. Inside the snyder-apps workspace, depend on @brandos/review-mode with workspace:*.

04 · Set it up

Five steps

1. A build-time flag

// vite.config.ts: 'on' for the dev server, refused in production builds
define: {
  'import.meta.env.VITE_REVIEW_MODE': JSON.stringify(reviewModeFor(command, mode)),
},

2. A manifest for what the route tree cannot express

// src/review/review.ts
import { defineReviewApp, type Note } from '@snyder-tech/brandos-review-mode/core';

export const reviewApp = defineReviewApp({
  name: 'YourApp',
  entries: [{ id: 'app', label: 'App', key: '1', first: '/' }],
  modules: [{ id: 'orders', label: 'Orders', sub: 'what customers bought' }],
  notes: import.meta.glob<{ default: readonly Note[] }>('../routes/**/*.notes.ts', { eager: true }),
});

3. One lazy mount at the root

// src/routes/__root.tsx
const ReviewModeMount =
  import.meta.env.VITE_REVIEW_MODE === 'on' ? lazy(() => import('../review/ReviewModeMount')) : null;

// src/review/ReviewModeMount.tsx
import { ReviewMode } from '@snyder-tech/brandos-review-mode/mount';
import { tanstackRouterAdapter } from '@snyder-tech/brandos-review-mode/tanstack-router';

export default function ReviewModeMount() {
  return <ReviewMode app={reviewApp} router={tanstackRouterAdapter(useRouter())} />;
}

4. Forced states at the data seam

import { useReviewState } from '@snyder-tech/brandos-review-mode/hooks';

const forced = useReviewState(); // 'loaded' outside review mode
const query = useQuery({ ...ordersQuery, enabled: forced === 'loaded' });

5. The design pages

import { ScreenMap, NavigationMap, NotesBacklog } from '@snyder-tech/brandos-review-mode/pages';
// Mount them on your own /design/$view route, lazily and behind the same flag.

05 · Entry points

Import only what each place needs

ImportContentsIn production
./coredefineReviewApp, reviewModeEnabled, activateReviewMode, the RouterAdapter contract, route catalogs and typesTypes only
./hooksuseReviewState, useVariant, useReviewRole, ReviewLink. No stylesheet.Yes, inert
./scenarioReviewScenario stand-ins for loading, empty, error and no accessNo
./mountReviewMode: the bar, notes layer, check panel and width stageNo
./pagesScreenMap, NavigationMap, NotesBacklog, DesignSystem, DesignIdeas, Brainstorm and planning pagesNo
./tanstack-routertanstackRouterAdapter(router)No
./tanstack-querywithReviewQueryClient(queryClient)No

06 · Routes

Screens come from the route tree

A route improves its entry through static data. Without it, the module is the first path segment and the label is the last one, title-cased. Routes under /design are never screens.

import type { ReviewRouteStaticData } from '@snyder-tech/brandos-review-mode/core';

declare module '@tanstack/react-router' {
  interface StaticDataRouteOption extends ReviewRouteStaticData {}
}

createFileRoute('/orders')({
  staticData: { module: 'orders', label: 'Orders', reviewStates: ['loaded', 'empty'] },
  component: Orders,
});

07 · Any router

Router adapters

Everything Review Mode needs from routing goes through one object. TanStack Router is bundled. For another router, implement these members; the live demo runs on a hash-based adapter of about forty lines.

type RouterAdapter = {
  routes: () => readonly ReviewRouteInfo[];
  getLocation: () => ReviewLocation; // same object until the location changes
  subscribe: (listener: () => void) => () => void;
  navigate: (target: { href: string } | { to: string; search?: Record<string, unknown> }) => void;
  Link: ComponentType<ReviewLinkProps>;
  frameHref?: (appHref: string) => string; // hash routers: '/orders' -> '/page#/orders'
};

08 · Production

Keep it out of production builds

  • Gate the mount, the design pages, ./scenario and any review-only server code on the build flag.
  • Make the production build refuse to turn the flag on.
  • Build for production and search the output for rvm- and the package name.
  • Allow same-origin framing on development responses so the width presets can frame the page.

09 · Keyboard

Shortcuts

Area keyEach entry’s own key, for example 1 or 2
TLight or dark
HHide or show the bar
WCycle width presets
NNotes layer
CSelf-check panel
EscClose the check panel