wcz-layout
Version:
260 lines (192 loc) • 6.99 kB
Markdown
name: layout-navigation
description: >
Configure LayoutProvider with theme, options, and Navigation structure.
Shell: AppBar with env chip + NavigationRail (permanent sm+, modal xs).
Navigation items: page (title, icon, to/href), header, divider.
LayoutOptions: showShell, snackbarOrigin, publicRoutes. Dark mode via
theme.applyStyles("dark", ...) with colorSchemeSelector
data-mui-color-scheme. i18n with i18next, locale files auto-detected.
Activate when configuring the layout shell, navigation, or theming.
type: core
library: wcz-layout
library_version: "7.6.1"
sources:
- "wcz-layout:src/providers/LayoutProvider.tsx"
- "wcz-layout:src/components/core/Layout.tsx"
- "wcz-layout:src/models/Navigation.ts"
- "wcz-layout:src/models/LayoutOptions.ts"
- "wcz-layout:src/lib/theme.ts"
# Layout & Navigation
## Setup
The layout shell is configured in the root route via `LayoutProvider`:
```typescript
// src/routes/__root.tsx
import { LayoutProvider, rootRouteHead, RouterNotFound, RouterError } from "wcz-layout";
import { theme } from "~/lib/theme";
import { navigation } from "~/navigation";
import type { LayoutOptions } from "wcz-layout/models";
const options: LayoutOptions = {
showShell: true,
publicRoutes: ["/login"],
};
export const Route = createRootRouteWithContext()({
head: rootRouteHead,
component: RootComponent,
notFoundComponent: RouterNotFound,
errorComponent: RouterError,
});
function RootComponent() {
return (
<LayoutProvider theme={theme} navigation={navigation} options={options}>
<Outlet />
</LayoutProvider>
);
}
```
## Core Patterns
### Navigation structure
Navigation is an array of items with three types — discriminated by the presence of `kind`:
```typescript
import type { Navigation } from "wcz-layout/models";
import HomeIcon from "@mui/icons-material/Home";
import ListIcon from "@mui/icons-material/List";
import SettingsIcon from "@mui/icons-material/Settings";
export const navigation: Navigation = [
{ title: "Home", icon: <HomeIcon />, to: "/" },
{ kind: "divider" },
{ kind: "header", title: "Management" },
{ title: "Todos", icon: <ListIcon />, to: "/todos" },
{ kind: "divider" },
{ title: "Settings", icon: <SettingsIcon />, to: "/settings" },
];
```
### Navigation item types
| Type | Shape | Required fields |
| --------- | --------------------------- | --------------- |
| Page item | `{ title, icon, to }` | `title`, `icon` |
| Divider | `{ kind: "divider" }` | `kind` |
| Header | `{ kind: "header", title }` | `kind`, `title` |
Page items also accept:
- `to` — internal route (TanStack Router link, type-safe)
- `href` — external URL (opens in new tab)
- `params`, `search` — TanStack Router link params
- `children` — nested navigation items (sub-menu)
- `hidden` — conditionally hide the item
### Nested navigation
```typescript
const navigation: Navigation = [
{
title: "Admin",
icon: <AdminIcon />,
to: "/admin",
children: [
{ title: "Users", icon: <PeopleIcon />, to: "/admin/users" },
{ title: "Roles", icon: <SecurityIcon />, to: "/admin/roles" },
],
},
];
```
### LayoutOptions
```typescript
interface LayoutOptions {
showShell?: boolean; // Show AppBar + NavigationRail (default: true)
snackbarOrigin?: SnackbarOrigin; // Notification position
publicRoutes?: string[]; // Routes that skip authentication
}
```
### Theme configuration
The theme is pre-configured in the template at `src/lib/theme.ts`. Use MUI's `extendTheme` to customize:
```typescript
// src/lib/theme.ts
import { extendTheme } from "@mui/material/styles";
export const theme = extendTheme({
colorSchemes: {
light: { palette: { primary: { main: "#1976d2" } } },
dark: { palette: { primary: { main: "#90caf9" } } },
},
colorSchemeSelector: "data-mui-color-scheme",
});
```
### Dark mode styling
Always use `theme.applyStyles("dark", ...)` for mode-specific styling:
```typescript
<Box
sx={(theme) => ({
backgroundColor: "#fff",
...theme.applyStyles("dark", {
backgroundColor: "#121212",
}),
})}
/>
```
### i18n setup
Locale files in `src/locales/` are auto-detected by `viteWczLayout()`:
```
src/locales/en.json # English (default)
src/locales/cs.json # Czech
```
The Vite plugin generates the `virtual:wcz-layout` module with all locale resources. `LayoutProvider` initializes `i18next` with language detection (cookie-based, 1-year expiry) and syncs Zod and DayJS locales on language change.
```typescript
import { useTranslation } from "react-i18next";
function MyComponent() {
const { t } = useTranslation();
return <Typography>{t("welcome")}</Typography>;
}
```
### Root route head
`rootRouteHead` provides meta tags and manifest link:
```typescript
export const Route = createRootRouteWithContext()({
head: rootRouteHead,
// optionally: head: rootRouteHead({ manifest: "/manifest.json" }),
});
```
### Service worker
`LayoutProvider` registers `/sw.js` on mount for PWA support.
## Common Mistakes
### HIGH Using theme.palette for dark mode instead of theme.applyStyles
Wrong:
```typescript
sx={{ color: mode === "dark" ? "white" : "black" }}
```
Correct:
```typescript
sx={(theme) => ({
color: "black",
...theme.applyStyles("dark", { color: "white" }),
})}
```
The app uses `colorSchemeSelector: "data-mui-color-scheme"`. Mode-specific styles must use `theme.applyStyles("dark", ...)`, not conditional palette checks or mode variables.
Source: copilot-instructions.md / wcz-layout:src/lib/theme.ts
Cross-skill: See also skills/ui-pages/SKILL.md § Common Mistakes
### HIGH Using href for internal routes in Navigation
Wrong:
```typescript
{ title: "Todos", icon: <ListIcon />, href: "/todos" }
```
Correct:
```typescript
{ title: "Todos", icon: <ListIcon />, to: "/todos" }
```
`NavigationPageItem` supports dual destination: `to` for internal TanStack Router links, `href` for external URLs. Using `href` for internal routes causes full page reloads.
Source: wcz-layout:src/models/Navigation.ts
### MEDIUM Creating NavigationPageItem without icon
Wrong:
```typescript
{ title: "Todos", to: "/todos" }
```
Correct:
```typescript
{ title: "Todos", icon: <ListIcon />, to: "/todos" }
```
`NavigationPageItem` requires both `title` and `icon`. Missing `icon` renders an empty slot in the NavigationRail.
Source: wcz-layout:src/models/Navigation.ts
### MEDIUM Adding locale without updating supportedLngs
This is actually **not** an issue — the Vite plugin auto-detects locale files in `src/locales/`. Adding a new JSON file (e.g. `de.json`) is sufficient; `supportedLngs` is derived from `Object.keys(resources)` automatically.
Source: wcz-layout:src/providers/LayoutProvider.tsx
See also:
- skills/ui-pages/SKILL.md — Pages render inside the layout shell.
- skills/auth/SKILL.md — publicRoutes bypass auth; permission guards need auth context.