Skip to content

AppShell

AppShell is the root component that wires together routing, navigation, authentication, and theming for your AppShell application. It should wrap your entire application layout.

Import

tsx
import { AppShell } from "@tailor-platform/app-shell";

Basic Usage

tsx
import { AppShell, SidebarLayout, defineModule } from "@tailor-platform/app-shell";

const modules = [
  defineModule({
    path: "dashboard",
    component: DashboardPage,
    meta: { title: "Dashboard" },
  }),
];

function App() {
  return (
    <AppShell title="My ERP App" basePath="/app" modules={modules}>
      <SidebarLayout />
    </AppShell>
  );
}

Props

title

  • Type: string (optional)
  • Description: Application title displayed in the sidebar header
tsx
<AppShell title="My ERP App" modules={modules}>
  {/* ... */}
</AppShell>

icon

  • Type: React.ReactNode (optional)
  • Description: Application icon displayed in the sidebar header
tsx
import { Building } from "lucide-react";

<AppShell title="My App" icon={<Building />} modules={modules}>
  {/* ... */}
</AppShell>;

basePath

  • Type: string (optional)
  • Default: ""
  • Description: Base path prefix for all routes
tsx
// Routes will be /app/dashboard, /app/products, etc.
<AppShell basePath="/app" modules={modules}>
  {/* ... */}
</AppShell>

// Routes will be /dashboard, /products, etc.
<AppShell basePath="" modules={modules}>
  {/* ... */}
</AppShell>

modules

  • Type: Module[] (required when not using file-based routing)
  • Description: Array of module definitions that define your application structure
tsx
import { defineModule, defineResource } from "@tailor-platform/app-shell";

const modules = [
  defineModule({
    path: "products",
    component: ProductsListPage,
    meta: { title: "Products" },
    resources: [
      defineResource({
        path: ":id",
        component: ProductDetailPage,
        meta: { title: "Product Details" },
      }),
    ],
  }),
];

<AppShell modules={modules}>{/* ... */}</AppShell>;

Learn more about Modules and Resources →

rootComponent

  • Type: () => React.ReactNode (optional)
  • Description: Component to render at the root path (e.g., /app/)
tsx
<AppShell basePath="/app" modules={modules} rootComponent={() => <HomePage />}>
  {/* ... */}
</AppShell>

Tip: For redirects from the root, use a guard with redirectTo() instead

tsx
import { redirectTo } from "@tailor-platform/app-shell";

<AppShell basePath="/app" modules={modules} rootComponent={() => redirectTo("/app/dashboard")}>
  {/* ... */}
</AppShell>;

settingsResources

  • Type: Resource[] (optional)
  • Description: Resources to include in the settings menu dropdown
tsx
import { defineResource } from "@tailor-platform/app-shell";

const settingsResources = [
  defineResource({
    path: "profile",
    component: ProfileSettingsPage,
    meta: { title: "Profile" },
  }),
  defineResource({
    path: "billing",
    component: BillingSettingsPage,
    meta: { title: "Billing" },
  }),
];

<AppShell modules={modules} settingsResources={settingsResources}>
  {/* ... */}
</AppShell>;

Settings appear in a dropdown menu in the sidebar header, accessible via the settings icon.

locale

  • Type: string (optional)
  • Default: Auto-detected from browser, falls back to "en"
  • Description: Locale code for built-in UI strings
tsx
<AppShell locale="ja" modules={modules}>
  {/* ... */}
</AppShell>

Supported locales: en, ja

Learn more about Internationalization →

errorBoundary

  • Type: ErrorBoundaryComponent (optional)
  • Description: Global error boundary component applied to all routes
tsx
import { useRouteError } from "@tailor-platform/app-shell";

const GlobalErrorBoundary = () => {
  const error = useRouteError() as Error;
  return (
    <div className="astw:p-8">
      <h1 className="astw:text-xl astw:font-bold astw:mb-4">Something went wrong</h1>
      <p className="astw:text-red-600">{error.message}</p>
    </div>
  );
};

<AppShell modules={modules} errorBoundary={GlobalErrorBoundary}>
  {/* ... */}
</AppShell>;

Note: Module and resource-level error boundaries take precedence over the global error boundary.

contextData

  • Type: ContextData (optional)
  • Description: Custom context data accessible from guards and components via useAppShell()

First, define your context data type using module augmentation:

typescript
// types.d.ts
declare module "@tailor-platform/app-shell" {
  interface AppShellRegister {
    contextData: {
      apiClient: ApiClient;
      currentUser: User | null;
      tenantId: string;
    };
  }
}

Then pass the data to AppShell:

tsx
// App.tsx
<AppShell
  modules={modules}
  contextData={{
    apiClient,
    currentUser,
    tenantId: "tenant-123",
  }}
>
  {/* ... */}
</AppShell>

Access the context data in your components:

tsx
import { useAppShell } from "@tailor-platform/app-shell";

function MyComponent() {
  const { context } = useAppShell();

  // Fully typed!
  const user = context.currentUser;
  const client = context.apiClient;

  return <div>Welcome, {user?.name}</div>;
}

Or in guards:

tsx
import { pass, hidden } from "@tailor-platform/app-shell";

const requireAuth: Guard = ({ context }) => {
  if (!context.currentUser) {
    return redirectTo("/login");
  }
  return pass();
};

searchSources

  • Type: readonly SearchSource[] (optional)
  • Description: Async search sources wired into the built-in CommandPalette. Each source is activated by typing its prefix followed by : in the palette. When active, the Actions and Pages sections are hidden and only results from that source are shown. The empty-input state of the palette lists all registered sources in a Search Modes section so users can discover them with a single click.
tsx
import { AppShell, type SearchSource } from "@tailor-platform/app-shell";

const searchSources: readonly SearchSource[] = [
  {
    prefix: "ORD",
    title: "Orders",
    search: async (query, { signal }) => {
      const results = await api.searchOrders(query, { signal });
      return results.map((o) => ({
        key: o.id,
        label: o.number,
        path: `/orders/${o.id}`,
      }));
    },
  },
];

<AppShell modules={modules} searchSources={searchSources}>
  <SidebarLayout sidebar={<DefaultSidebar />} />
</AppShell>;

See SearchSource for the full type reference.

Note: DefaultSidebar always renders a Search entry that opens the palette regardless of whether searchSources is configured. The Cmd+K / Ctrl+K shortcut also works globally.

children

  • Type: React.ReactNode (required)
  • Description: Layout component that renders your application content

Typically, you'll use SidebarLayout:

tsx
import { SidebarLayout } from "@tailor-platform/app-shell";

<AppShell modules={modules}>
  <SidebarLayout />
</AppShell>;

Or create a custom layout:

tsx
<AppShell modules={modules}>
  <CustomLayout />
</AppShell>

File-Based Routing Mode

When using the Vite plugin for file-based routing, use AppShell exactly as you would without the plugin — no modules prop required. The plugin intercepts the @tailor-platform/app-shell import and wraps AppShell with pages pre-configured via AppShell.WithPages() internally.

tsx
// App.tsx
import { AppShell, SidebarLayout, DefaultSidebar } from "@tailor-platform/app-shell";

// No modules prop needed — the plugin injects pages automatically
<AppShell title="My App">
  <SidebarLayout sidebar={<DefaultSidebar />} />
</AppShell>;

Note: AppShell.WithPages is an internal method used by the Vite plugin. Do not call it directly in application code.

Learn more about File-Based Routing →

Complete Example

Here's a complete example with all common features:

tsx
import {
  AppShell,
  SidebarLayout,
  defineModule,
  defineResource,
  useRouteError,
} from "@tailor-platform/app-shell";
import { Building } from "lucide-react";

// Define context data type
declare module "@tailor-platform/app-shell" {
  interface AppShellRegister {
    contextData: {
      currentUser: User | null;
    };
  }
}

// Error boundary
const ErrorBoundary = () => {
  const error = useRouteError() as Error;
  return (
    <div className="astw:p-8">
      <h1 className="astw:text-xl astw:font-bold">Error</h1>
      <p>{error.message}</p>
    </div>
  );
};

// Modules
const modules = [
  defineModule({
    path: "dashboard",
    component: DashboardPage,
    meta: { title: "Dashboard" },
  }),
  defineModule({
    path: "products",
    component: ProductsPage,
    meta: { title: "Products" },
    resources: [
      defineResource({
        path: ":id",
        component: ProductDetailPage,
      }),
    ],
  }),
];

// Settings
const settingsResources = [
  defineResource({
    path: "profile",
    component: ProfilePage,
    meta: { title: "Profile" },
  }),
];

// App
function App() {
  const currentUser = useCurrentUser();

  return (
    <AppShell
      title="My ERP App"
      icon={<Building />}
      basePath="/app"
      modules={modules}
      settingsResources={settingsResources}
      locale="en"
      errorBoundary={ErrorBoundary}
      contextData={{ currentUser }}
    >
      <SidebarLayout />
    </AppShell>
  );
}

export default App;

API Reference