# App Layout — Vite scaffold (beginner-friendly)

## Fastest path — scaffold a full project (recommended)

**Do not** start from plain `vite react-ts` when the user wants a new Impact Nova dashboard.

### Option A — MCP tool (Cursor / agent)

Call **`scaffold_impact_nova_app`** with `projectName`, optional `targetDirectory`, and **`modules[]`** (recipe + slug + title from the user prompt). Auto-detects npm vs monorepo `file:` link.

This writes a **runnable project shell** plus optional recipe modules:

| Pre-wired (base template) | Details |
|-----------|---------|
| Vite + React 19 + TypeScript | `package.json`, `vite.config.ts`, tsconfigs |
| Tailwind + Impact Nova preset | `tailwind.config.js`, `postcss.config.js` |
| Impact Nova CSS + i18n + Manrope | `main.tsx` |
| `Layout` shell | Sidebar + Header + React Router |
| **Premium page chrome** | `PageShell`, `PageStickyHeader`, `EmptyStateView`, `SummaryMetricCard` |
| Home page | Welcome empty state with CTAs |
| Shared DataTable primitives | Density, viewport, cell registry, filter guard |
| AG Grid license hook | `setup-ag-grid.ts` + `.env.example` |

| Recipe modules (`modules[]`) | Details |
|------------------------------|---------|
| `filtered-workspace` | 3-phase planning: empty → FilterPanel → KPI + grid |
| `data-catalog` | FilterStrip + DataTable |
| `dashboard` | KPI cards + Chart |

Then: `cd <project> && npm run dev` (or let the tool run `npm install`).

### Option B — CLI

```bash
npx create-impact-nova my-dashboard
cd my-dashboard
npm run dev
```

Inside monorepo: auto `file:` link. From npm: auto `impact-nova@^2.5.10`. Override with `--from-npm` or `--link-monorepo`.

---

## Manual setup (only if scaffolding is unavailable)

Use **`Layout`** from `impact-nova/layout` as the app shell. Compose **Sidebar**, **Header**, **Breadcrumb**, and **FilterStrip** from their subpaths inside page `children`.

Fetch this resource for architecture and copy-paste fallback.

---

## Architecture

```
SidebarProvider (app root)
├── Layout
│   ├── sidebar  → <Sidebar> (nav rail)
│   ├── header   → <Header> (logo, title, actions)
│   └── children → pages (BreadcrumbHeader, FilterStrip, page body)
├── CommandPalette (optional, sibling)
└── overlays (NotificationPanel, FilterPanel, Sheet…)
```

**Rules:**
- `SidebarProvider` wraps the **entire** layout at the app root — not inside the sidebar slot.
- `Layout` only provides structure: sidebar column | header + scrollable main.
- Each page composes its own breadcrumb bar and filter strip inside `children`.

---

## Step 1 — Scaffold (preferred) or create Vite project

```bash
npm create impact-nova@latest my-dashboard
cd my-dashboard
npm run dev
```

Manual fallback only:

```bash
npm create vite@latest my-dashboard -- --template react-ts
cd my-dashboard
```

---

## Step 2 — Install Impact Nova

```bash
npm install impact-nova impact-nova-icons react@^19 react-dom@^19
```

Optional (data tables, charts):

```bash
npm install ag-grid-community@36.0.1 ag-grid-react@36.0.1 ag-grid-enterprise@36.0.1
```

---

## Step 3 — `vite.config.ts`

```ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  resolve: {
    dedupe: ['react', 'react-dom', 'ag-grid-community', 'ag-grid-enterprise', 'ag-grid-react'],
  },
  optimizeDeps: {
    exclude: ['impact-nova-icons'],
  },
});
```

---

## Step 4 — `index.html` (Manrope font)

```html
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@200..800&display=swap" rel="stylesheet" />
```

---

## Step 5 — `src/main.tsx`

```tsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import 'impact-nova/dist/impact-nova.css';
import './index.css';
import App from './App';
import { ImpactNovaProviders } from 'impact-nova/form';

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <ImpactNovaProviders locale="en">
      <App />
    </ImpactNovaProviders>
  </StrictMode>,
);
```

---

## Step 6 — `src/index.css`

```css
body {
  margin: 0;
  font-family: 'Manrope', sans-serif;
}

#root {
  height: 100vh;
}
```

---

## Step 7 — `src/App.tsx` (full scaffold)

```tsx
import { useState } from 'react';
import { Layout } from 'impact-nova/layout';
import {
  Sidebar,
  SidebarProvider,
  SidebarHeader,
  SidebarTrigger,
  SidebarContent,
  SidebarFooter,
  SidebarSeparator,
  SidebarLogout,
} from 'impact-nova/sidebar';
import {
  Header,
  HeaderLeft,
  HeaderRight,
  HeaderLogo,
  HeaderSeparator,
  HeaderTitle,
  NotificationIconButton,
} from 'impact-nova/header';
import {
  Breadcrumb,
  BreadcrumbList,
  BreadcrumbItem,
  BreadcrumbLink,
  BreadcrumbSeparator,
  BreadcrumbPage,
  BreadcrumbHeader,
  HomeIcon,
} from 'impact-nova/breadcrumb';
import { FilterStrip } from 'impact-nova/filter-strip';
import { Avatar, AvatarFallback } from 'impact-nova/avatar';
import { Button } from 'impact-nova/button';
import { Blocks, Settings, IAUrl } from 'impact-nova-icons';

const NAV_ROUTES = [
  { value: 'dashboard', label: 'Dashboard', icon: <Blocks size={20} />, url: '#dashboard' },
  { value: 'settings', label: 'Settings', icon: <Settings size={20} />, url: '#settings' },
];

function AppSidebar({ activePage, onNavigate }: { activePage: string; onNavigate: (value: string) => void }) {
  return (
    <Sidebar collapsible="offcanvas">
      <SidebarHeader>
        <SidebarTrigger />
      </SidebarHeader>
      <SidebarContent routes={NAV_ROUTES} activeValue={activePage} onNavigate={onNavigate} />
      <SidebarFooter>
        <SidebarSeparator />
        <SidebarLogout onClick={() => console.log('logout')} />
      </SidebarFooter>
    </Sidebar>
  );
}

function AppHeader() {
  return (
    <Header>
      <HeaderLeft>
        <HeaderLogo>
          <img src={IAUrl} alt="" width={91} height={36} className="h-9 w-auto" />
        </HeaderLogo>
        <HeaderSeparator />
        <HeaderTitle>My App</HeaderTitle>
      </HeaderLeft>
      <HeaderRight>
        <NotificationIconButton showIndicator />
        <Avatar size="sm">
          <AvatarFallback>JD</AvatarFallback>
        </Avatar>
      </HeaderRight>
    </Header>
  );
}

function DashboardPage() {
  return (
    <>
      <div className="sticky top-0 z-40 bg-canvas-wash">
        <BreadcrumbHeader>
          <Breadcrumb>
            <BreadcrumbList>
              <BreadcrumbItem>
                <BreadcrumbLink href="/"><HomeIcon /></BreadcrumbLink>
              </BreadcrumbItem>
              <BreadcrumbSeparator />
              <BreadcrumbItem>
                <BreadcrumbPage>Dashboard</BreadcrumbPage>
              </BreadcrumbItem>
            </BreadcrumbList>
          </Breadcrumb>
        </BreadcrumbHeader>
      </div>
      <div className="p-8">
        <h1 className="text-2xl font-bold">Welcome</h1>
      </div>
    </>
  );
}

function ItemsPage() {
  const [filters, setFilters] = useState([
    { id: '1', label: 'Status', value: 'Active', removable: true },
  ]);

  return (
    <>
      <div className="sticky top-0 z-40 bg-canvas-wash">
        <BreadcrumbHeader>
          <Breadcrumb>
            <BreadcrumbList>
              <BreadcrumbItem>
                <BreadcrumbLink href="/"><HomeIcon /></BreadcrumbLink>
              </BreadcrumbItem>
              <BreadcrumbSeparator />
              <BreadcrumbItem>
                <BreadcrumbPage>Items</BreadcrumbPage>
              </BreadcrumbItem>
            </BreadcrumbList>
          </Breadcrumb>
        </BreadcrumbHeader>
        <FilterStrip
          filters={filters}
          onFilterRemove={(filterId) => setFilters((previous) => previous.filter((f) => f.id !== filterId))}
          onClearAll={() => setFilters([])}
          onAllFiltersClick={() => {}}
          savedFilters={[]}
        />
      </div>
      <div className="p-8">
        <h1 className="text-2xl font-bold">Items</h1>
      </div>
    </>
  );
}

export default function App() {
  const [activePage, setActivePage] = useState('dashboard');

  return (
    <SidebarProvider
      defaultOpen={false}
      id="app-sidebar"
      className="flex h-svh min-h-0 w-full min-w-0 overflow-hidden"
    >
      <Layout
        sidebar={<AppSidebar activePage={activePage} onNavigate={setActivePage} />}
        header={<AppHeader />}
      >
        {activePage === 'dashboard' ? <DashboardPage /> : <ItemsPage />}
      </Layout>
    </SidebarProvider>
  );
}
```

---

## Imports cheat sheet

| Piece | Import from |
|-------|-------------|
| App shell | `impact-nova/layout` → `Layout` |
| Sidebar | `impact-nova/sidebar` |
| Header | `impact-nova/header` |
| Breadcrumb | `impact-nova/breadcrumb` |
| Filter strip | `impact-nova/filter-strip` |
| Filter panel | `impact-nova/filter-panel` |
| Data table | `impact-nova/data-table` |
| Icons | `impact-nova-icons` |
| CSS | `impact-nova/dist/impact-nova.css` |

---

## With React Router

```tsx
import { BrowserRouter, Routes, Route, Outlet } from 'react-router';

<SidebarProvider defaultOpen={false} id="app-sidebar" className="flex h-svh min-h-0 w-full min-w-0 overflow-hidden">
  <Layout sidebar={<AppSidebar />} header={<AppHeader />}>
    <Suspense fallback={<Loader />}>
      <Outlet />
    </Suspense>
  </Layout>
</SidebarProvider>
```

Each route page renders its own `BreadcrumbHeader` + `FilterStrip` + body inside the outlet.

---

## With Command Palette

Wrap with `CommandPaletteProvider` at the root; render `<CommandPalette />` as a sibling inside `SidebarProvider`. See `impact-nova://command-palette`.

---

## Common mistakes

| Mistake | Fix |
|---------|-----|
| `SidebarProvider` inside sidebar slot | Move `SidebarProvider` to app root around `Layout` |
| Breadcrumb inside `Layout` props | Put breadcrumb in page `children` |
| Barrel import `from 'impact-nova'` in features | Use subpaths: `impact-nova/button`, `impact-nova/sidebar` |
| Missing CSS import | Add `import 'impact-nova/dist/impact-nova.css'` in `main.tsx` |
| `DynamicLayout` for app shell | `DynamicLayout` is a grid/flex utility — use `Layout` for app chrome |

---

## Storybook reference

See `Layout/Layout` stories in Impact Nova Storybook for header variants, filter strip collapse, empty/loading states, and scroll behavior.
