@matteoaliano/forest-ui 0.8.9 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/theme.mjs CHANGED
@@ -18,10 +18,7 @@ import {
18
18
  fontFamilyMono,
19
19
  fontSize,
20
20
  fontWeight,
21
- forestAgencyPreset,
22
21
  forestAlkemyPlusPreset,
23
- forestExternalPreset,
24
- forestInternalPreset,
25
22
  forestTheme,
26
23
  forestThemeOptions,
27
24
  getAvailablePresets,
@@ -45,7 +42,7 @@ import {
45
42
  violet,
46
43
  warning,
47
44
  widths
48
- } from "./chunk-2XQSGP7X.mjs";
45
+ } from "./chunk-XJYBU6LC.mjs";
49
46
  export {
50
47
  alpha,
51
48
  avatarColors,
@@ -66,10 +63,7 @@ export {
66
63
  fontFamilyMono,
67
64
  fontSize,
68
65
  fontWeight,
69
- forestAgencyPreset,
70
66
  forestAlkemyPlusPreset,
71
- forestExternalPreset,
72
- forestInternalPreset,
73
67
  forestTheme,
74
68
  forestThemeOptions,
75
69
  getAvailablePresets,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matteoaliano/forest-ui",
3
- "version": "0.8.9",
3
+ "version": "1.1.0",
4
4
  "description": "Forest Design System — themed MUI components",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -30,7 +30,7 @@
30
30
  "fonts"
31
31
  ],
32
32
  "scripts": {
33
- "sync": "node bin/sync.mjs",
33
+ "sync": "node bin/sync.mjs --internal",
34
34
  "lint": "eslint src --ext .ts,.tsx",
35
35
  "build": "tsup",
36
36
  "prepublishOnly": "tsup && node bin/sync.mjs",
@@ -3,139 +3,52 @@ name: forest-alkemy-plus
3
3
  description: Forest UI Design System rules for the Alkemy+ (charcoal/red) preset. Enforces correct imports, component usage, and theming with @matteoaliano/forest-ui. Use when the project uses forest-ui, forest-alkemy-plus preset, or when user builds UI components in a forest-ui project. Triggers on "forest", "forest-ui", "forest alkemy", "forest alkemy+", "@matteoaliano/forest-ui".
4
4
  metadata:
5
5
  author: Forest Design System
6
- version: 0.8.9
6
+ version: 1.1.0
7
7
  ---
8
8
 
9
9
  # Forest UI — Alkemy+ Preset
10
10
 
11
- ## Golden Rules
11
+ ## Golden Rules (non-negotiable)
12
12
 
13
- 1. **NEVER import from `@mui/material` directly.** Always import from `@matteoaliano/forest-ui`. All MUI components, layout primitives, transitions, form helpers, and hooks are re-exported. The only exception is `@mui/icons-material` import icons from there directly.
14
- 2. **ALWAYS import the Forest font CSS at app entry** (see Setup → Step 2). The package bundles Aeonik / Aeonik Mono / AlkemyBETA as `.woff2` + `@font-face` CSS, but it does **not** auto-inject them — the consuming app must import the CSS. Skip this and `font-family: "Aeonik"` silently falls back to system sans-serif (Helvetica / Arial / DejaVu), inconsistently across machines. This is the #1 reason Forest sites "don't look right".
15
- 3. **ALWAYS wrap your app root with `<ForestProvider>`** it applies the theme and CSS baseline.
16
- 4. **NEVER use inline colors or spacing values.** Use design tokens or MUI's `sx` prop with theme values (`p: 4`, `backgroundColor: "primary.main"`).
17
- 5. **NEVER create custom component wrappers** for things Forest UI already provides.
18
- 6. **TypeScript is required.** All components export their prop types (e.g. `type ButtonProps`).
19
- 7. **ALWAYS use the Outlined variant of MUI icons.** Import from `@mui/icons-material/*Outlined` (e.g. `CloseOutlined`, `MailOutlined`). Never use filled, Rounded, Sharp, or TwoTone variants.
13
+ 1. **Import only from `@matteoaliano/forest-ui`** never `@mui/material`. Icons only from `@mui/icons-material/*Outlined` (always the Outlined variant).
14
+ 2. **Wrap the app root in `<ForestProvider preset="forest-alkemy+">`** and import the three font CSS files at entry (below). Skipping the fonts is the #1 "looks wrong" cause.
15
+ 3. **Every authenticated surface is `<AppShell>`; every routed page is `<Page>`.** Never hand-roll app chrome or page scaffolds. The color-mode toggle is built into AppShell — never add your own.
16
+ 4. **Every widget, chart, or table inside a `<Page>` lives in a `<Card>`.** A clickable card wraps its content in `<CardActionArea>`.
17
+ 5. **No raw colors or spacing** (`#hex`, `rgb()`, `"16px"`) use theme tokens via `sx` (`p: 4`, `color: "text.secondary"`, `backgroundColor: "primary.main"`). Never override a component's internals.
18
+ 6. **Dark mode is built in** `defaultMode` on ForestProvider + `useColorScheme`. Never `createTheme`, a second `ThemeProvider`, or `.dark` CSS.
20
19
 
21
- ## Setup
22
-
23
- ### Step 1 — Install
24
-
25
- ```bash
26
- npm install @matteoaliano/forest-ui \
27
- @mui/material@^7 @mui/icons-material@^7 \
28
- @mui/x-charts@^7 @mui/x-data-grid@^7 @mui/x-date-pickers@^8 \
29
- @emotion/react @emotion/styled dayjs
30
- ```
31
-
32
- > **Note:** Every `@mui/*` package above is a **peer dependency** of `@matteoaliano/forest-ui`. Install them at the pinned major versions shown — newer majors are not yet supported and will fail `npm install` with `ERESOLVE`. Always import components, hooks, and layout primitives from `@matteoaliano/forest-ui` (Forest UI re-exports them with branded defaults). The single exception is `@mui/icons-material` — import icons from there directly, using the Outlined variant (e.g. `import { CloseOutlined } from "@mui/icons-material"`).
33
-
34
- ### Step 2 — Load the fonts (CRITICAL — do this before anything else)
35
-
36
- The Forest preset sets `fontFamily: '"Aeonik", sans-serif'`. Without the matching `@font-face` rules, the browser cannot resolve "Aeonik" and silently falls back to a system font. **Forest UI ships the font files in the npm tarball, but the consuming app must import the CSS.** Add these three imports at the top of your root entry file:
20
+ ## Setup (once per app)
37
21
 
38
22
  ```tsx
39
- // Next.js App Router app/layout.tsx (above any other CSS imports)
40
- // Next.js Pages Router → pages/_app.tsx
41
- // Vite / CRA → src/main.tsx or src/index.tsx
23
+ // App entry fonts first, then the provider:
42
24
  import "@matteoaliano/forest-ui/fonts/aeonik/aeonik.css";
43
25
  import "@matteoaliano/forest-ui/fonts/aeonik-mono/aeonik-mono.css";
44
26
  import "@matteoaliano/forest-ui/fonts/alkemy-beta/alkemy-beta.css";
45
- ```
46
-
47
- No `public/fonts/` copy, no `next/font/local`, no manual `@font-face` rules — the bundler resolves the relative `.woff2` URLs inside the CSS automatically.
48
-
49
- **For Next.js**, also add `transpilePackages` so Turbopack resolves the CSS subpath exports cleanly:
27
+ import { ForestProvider } from "@matteoaliano/forest-ui";
50
28
 
51
- ```ts
52
- // next.config.ts
53
- const nextConfig = {
54
- transpilePackages: ["@matteoaliano/forest-ui"],
55
- experimental: {
56
- optimizePackageImports: [
57
- "@matteoaliano/forest-ui",
58
- "@mui/material",
59
- "@mui/icons-material",
60
- ],
61
- },
62
- };
63
- export default nextConfig;
29
+ <ForestProvider preset="forest-alkemy+" defaultMode="system">{app}</ForestProvider>
64
30
  ```
65
31
 
66
- **Verify it worked.** Open DevTools after running `next dev`:
67
-
68
- 1. **Network tab → filter "font"** — you should see `aeonik-regular.woff2` and friends loading with `200 OK` from `/_next/static/media/...` (or your bundler's asset path).
69
- 2. **Elements → Computed → font-family** on a `<Typography>` — should read `"Aeonik", sans-serif`.
70
- 3. **Elements → Rendered Fonts** (Chrome only) — should say `Aeonik — Web font`, not `Arial — Local file` or `Helvetica — Local file`.
71
-
72
- If Network shows 404s on the woff2 files, the `transpilePackages` step is missing or the bundler isn't picking up the package's subpath exports.
32
+ Install: `npm install @matteoaliano/forest-ui @mui/material@^7 @mui/icons-material@^7 @mui/x-charts@^7 @mui/x-data-grid@^7 @mui/x-date-pickers@^8 @emotion/react @emotion/styled dayjs`. Next.js needs `transpilePackages: ["@matteoaliano/forest-ui"]` — details + font troubleshooting in `references/patterns.md`.
73
33
 
74
- ### Step 3 Wrap your app
34
+ ## Layout baseline (mandatory shape of every screen)
75
35
 
76
36
  ```tsx
77
- import { ForestProvider } from "@matteoaliano/forest-ui";
78
-
79
- function App() {
80
- return (
81
- <ForestProvider preset="forest-alkemy+">
82
- {/* All app content here */}
83
- </ForestProvider>
84
- );
85
- }
37
+ <AppShell navItems={[{ label: "Home", icon: <HomeOutlined />, href: "/", active: true }]} search actions={...}>
38
+ <Page title="Home" breadcrumbs={[{ label: "Home" }]}>
39
+ <Card>…every widget/chart/table goes in a Card…</Card>
40
+ </Page>
41
+ </AppShell>
86
42
  ```
87
43
 
88
- ## Typography & Fonts
89
-
90
- - **Aeonik** — the default font for all UI text (headings, body, labels, buttons, etc.). Family name: `"Aeonik"`.
91
- - **Aeonik Mono** — use for numeric values: prices, stats, table figures, counters, dates, IDs, code snippets. Family name: `"Aeonik Mono"`.
92
- - **AlkemyBETA** — editorial display face for page titles / hero h1. Family name: **`"AlkemyBETA"`** (one word, no space — do not write `"Alkemy Beta"`, it will not resolve). Not auto-applied by the preset — opt in explicitly:
93
- ```tsx
94
- <Typography variant="h1" sx={{ fontFamily: '"AlkemyBETA", serif' }}>...</Typography>
95
- ```
96
-
97
- The three CSS imports in Setup → Step 2 register all of these families. If a Forest site looks like Arial or Helvetica, you skipped Step 2.
98
-
99
- ## Available Components
100
-
101
- **Inputs:** Button, ButtonGroup, TextField, Select + MenuItem, MultiSelect, Checkbox, RadioGroup + Radio, Switch, ToggleButton + ToggleButtonGroup, Fab, IconButton, Autocomplete, Search, DatePicker, Input
102
-
103
- **Data Display:** Badge, Chip, Divider, Typography, Tooltip, Logo, Table family (Table, TableHead, TableBody, TableRow, TableCell, TableContainer, TableFooter, TablePagination, TableSortLabel), DataGrid + GridColDef, List family (List, ListItem, ListItemButton, ListItemIcon, ListItemText, ListItemAvatar, ListItemSecondaryAction, ListSubheader), Avatar + AvatarGroup, ImageList + ImageListItem + ImageListItemBar, Rating
104
-
105
- **Surfaces:** Accordion family, AppBar + Toolbar + AppBarNavItem, Card family + CardActionArea, Paper, Drawer + SwipeableDrawer
106
-
107
- **Feedback:** Alert + AlertTitle, Dialog family, Backdrop, LinearProgress, CircularProgress, Skeleton, Modal, Popover, Snackbar + SnackbarContent, Slider
108
-
109
- **Navigation:** Breadcrumbs, Link, Menu + MenuList, Pagination + PaginationItem, Stepper family (Stepper, Step, StepLabel, StepButton, StepConnector, StepContent, StepIcon), Tabs + Tab + TabScrollButton, SidebarNav + SidebarItem + useSidebar, BottomNavigation + BottomNavigationAction, SpeedDial + SpeedDialAction + SpeedDialIcon, MobileStepper
110
-
111
- **Layout:** Box, Stack, Grid, Container
112
-
113
- **Form Helpers:** FormControl, FormControlLabel, FormGroup, FormHelperText, FormLabel, InputAdornment, InputBase, InputLabel, OutlinedInput, FilledInput
114
-
115
- **Transitions:** Collapse, Fade, Grow, Slide, Zoom
116
-
117
- **Utilities:** ClickAwayListener, NoSsr, Portal, Popper, TextareaAutosize, SvgIcon, ButtonBase, GlobalStyles
118
-
119
- **Hooks:** useMediaQuery, useScrollTrigger, useFormControl, useTheme
120
-
121
- **Charts:** BarChart, LineChart, PieChart, ScatterChart, Gauge, SparkLineChart, useChartColors, plus composition primitives (see components.md)
122
-
123
- See `references/components.md` for full API details, `references/patterns.md` for code examples, and `references/upgrading.md` for update / re-sync guidance.
44
+ Full APIs (nav router escape, logoVariant, brand/search/actions slots, Page actions/maxWidth) in `references/patterns.md` → "App Shell + Page".
124
45
 
125
- ## Tooltip vs Popover
46
+ ## Fonts
126
47
 
127
- - **Tooltip** Use for **text-only hints**. The Tooltip has a dark (black) background and is meant for short, plain-text labels or descriptions. Do not nest rich content inside a Tooltip.
128
- - **Popover** — Use when you need to display **rich or interactive content** such as Chips, lists, buttons, or any nested components. Popover renders in a neutral surface container that supports arbitrary children.
48
+ **Aeonik** = all UI text. **Aeonik Mono** = numbers (prices, stats, dates, IDs, code). **`"AlkemyBETA"`** (one word!) = hero/h1 titles only, opt-in via `sx={{ fontFamily: '"AlkemyBETA", serif' }}`.
129
49
 
130
- **Rule of thumb:** If the overlay content is just a string, use `<Tooltip>`. If it contains components, use `<Popover>`.
50
+ ## Components
131
51
 
132
- ## Common Anti-Patterns
52
+ Inputs: Button, ButtonGroup, TextField, Select+MenuItem, MultiSelect, Checkbox, RadioGroup, Switch, ToggleButton, Fab, IconButton (variant: text|contained|outlined), Autocomplete, Search, DatePicker, Input · Data: Badge, Chip, Divider, Typography, Tooltip (plain text only; rich content → Popover), Logo, Table family, DataGrid, List family, Avatar, ImageList, Rating · Surfaces: Accordion, AppBar+Toolbar, Card family, Paper, Drawer · Feedback: Alert, Dialog, Backdrop, Progress, Skeleton, Modal, Popover, Snackbar, Slider · Navigation: Breadcrumbs, Link, Menu, Pagination, Stepper, Tabs, SidebarNav+SidebarItem, BottomNavigation, SpeedDial · Layout: **AppShell, Page**, Box, Stack, Grid, Container · Charts: BarChart, LineChart, PieChart, ScatterChart, Gauge, SparkLineChart · plus form helpers, transitions, utilities, hooks (useMediaQuery, useTheme, useColorScheme).
133
53
 
134
- 1. **Missing the three font-CSS imports at app entry** the most common Forest setup mistake. Aeonik silently falls back to system sans-serif and the UI looks "off" without any console error. See Setup → Step 2 for the imports and a DevTools verification checklist.
135
- 2. **Writing the AlkemyBETA family with a space** (`"Alkemy Beta"`) — the `@font-face` declares `"AlkemyBETA"` (one word). The spaced form fails to resolve.
136
- 3. **Importing from `@mui/material`** instead of `@matteoaliano/forest-ui` — all components, hooks, and layout primitives are available from forest-ui
137
- 4. **Hardcoded color values** (`backgroundColor: "#7f56d9"`) instead of theme tokens (`backgroundColor: "primary.main"`)
138
- 5. **Hardcoded spacing** (`padding: "16px"`) instead of theme spacing (`p: 4`)
139
- 6. **Missing `<ForestProvider>`** at the app root — theme won't apply
140
- 7. **Using filled MUI icons** (`Close`) instead of Outlined (`CloseOutlined`)
141
- 8. **Building custom multi-select** instead of using the `<MultiSelect>` component
54
+ **Full prop tables:** `references/components.md` · **Copy-paste patterns** (forms, tables, dialogs, app shell, detail drawer, charts): `references/patterns.md` · **Upgrading:** `references/upgrading.md`.
@@ -18,6 +18,8 @@ import Button from "@mui/material/Button";
18
18
 
19
19
  | Component | Import | Key Props |
20
20
  |-----------|--------|-----------|
21
+ | `AppShell` | `import { AppShell, type AppShellNavItem } from "@matteoaliano/forest-ui"` | **Mandatory app frame.** `navItems: AppShellNavItem[]` (`{ label, icon, active?, href?, onClick?, component?, endAdornment? }` — `component` is the router escape), `nav` (custom sidebar node, overrides navItems), `product`, `logoVariant: "logomark" \| "full"`, `brand` (custom node), `search: boolean \| ReactNode`, `actions`, `sidebarBehavior: "permanent" \| "hover"`, `defaultSidebarOpen`. Renders the color-mode toggle itself (not removable). Children = a `<Page>` |
22
+ | `Page` | `import { Page, type PageBreadcrumb } from "@matteoaliano/forest-ui"` | **Mandatory page scaffold.** `title` (required, h5), `breadcrumbs: PageBreadcrumb[]` (`{ label, href?, onClick?, component? }`; last crumb = plain text), `actions` (right-aligned header node), `disableGutters`, `maxWidth`, `sx`. Owns `background.paper` + 24px gutters. Widgets/charts/tables inside go in `<Card>`s |
21
23
  | `Box` | `import { Box } from "@matteoaliano/forest-ui"` | `sx`, `component`, all HTML attributes |
22
24
  | `Stack` | `import { Stack } from "@matteoaliano/forest-ui"` | `direction`, `spacing`, `divider`, `alignItems`, `justifyContent` |
23
25
  | `Grid` | `import { Grid } from "@matteoaliano/forest-ui"` | `container`, `size`, `spacing`, `offset`, `columns`, `direction` |
@@ -52,7 +54,7 @@ import Button from "@mui/material/Button";
52
54
  | `Switch` | `import { Switch } from "@matteoaliano/forest-ui"` | `color: "default" \| "primary" \| "secondary" \| "error" \| "info" \| "success" \| "warning"`, `size: "small" \| "medium"`, `checked`, `disabled` | — |
53
55
  | `ToggleButton` + `ToggleButtonGroup` | `import { ToggleButton, ToggleButtonGroup } from "@matteoaliano/forest-ui"` | `value`, `exclusive`, `onChange`, `orientation: "horizontal" \| "vertical"`, `size: "small" \| "medium" \| "large"`, `color`, `fullWidth` | — |
54
56
  | `Fab` | `import { Fab } from "@matteoaliano/forest-ui"` | `variant: "circular" \| "extended"`, `size: "small" \| "medium" \| "large"`, `color`, `disabled`, `href` | `color="primary"` |
55
- | `IconButton` | `import { IconButton } from "@matteoaliano/forest-ui"` | `color: "inherit" \| "primary" \| "secondary" \| "error" \| "info" \| "success" \| "warning" \| "default"`, `size: "small" \| "medium" \| "large"`, `disabled`, `disableRipple` | |
57
+ | `IconButton` | `import { IconButton } from "@matteoaliano/forest-ui"` | `variant: "text" \| "contained" \| "outlined"` (contained/outlined get the faux-3D button affordance; text is the bare icon), `color: "inherit" \| "primary" \| "secondary" \| "error" \| "info" \| "success" \| "warning" \| "default"`, `size: "small" \| "medium" \| "large"`, `disabled`, `disableRipple` | `variant="text"` |
56
58
  | `Autocomplete` | `import { Autocomplete } from "@matteoaliano/forest-ui"` | `options`, `renderInput` (required), `multiple`, `freeSolo`, `disableClearable`, `loading`, `groupBy`, `limitTags`, `filterOptions` | — |
57
59
  | `Search` | `import { Search } from "@matteoaliano/forest-ui"` | `onClear`, `placeholder`, all TextField props (except `variant`) | `variant="outlined"`, `placeholder="Search..."`, built-in search icon and clear button |
58
60
  | `Input` | `import { Input } from "@matteoaliano/forest-ui"` | `value`, `onChange`, `placeholder`, `multiline`, `rows`, `startAdornment`, `endAdornment`, `disabled`, `error` — all InputBase props | — |
@@ -90,9 +92,9 @@ import Button from "@mui/material/Button";
90
92
  | `Accordion` family | `import { Accordion, AccordionSummary, AccordionDetails, AccordionActions } from "@matteoaliano/forest-ui"` | `expanded`, `defaultExpanded`, `onChange`, `disabled`, `disableGutters`, `square` | — |
91
93
  | `AppBar` + `Toolbar` | `import { AppBar, Toolbar } from "@matteoaliano/forest-ui"` | `position`, `color`, `elevation`; Toolbar: `variant: "dense" \| "regular"` | — |
92
94
  | `AppBarNavItem` | `import { AppBarNavItem } from "@matteoaliano/forest-ui"` | `label` (required), `icon`, `selected`, `href`, `sx` — navigation button for AppBar | — |
93
- | `Card` family | `import { Card, CardContent, CardHeader, CardActions, CardMedia } from "@matteoaliano/forest-ui"` | `variant: "elevation" \| "outlined"`, `elevation`, `raised`; CardHeader: `title`, `subheader`, `avatar`, `action` | — |
95
+ | `Card` family | `import { Card, CardContent, CardHeader, CardActions, CardMedia } from "@matteoaliano/forest-ui"` | `variant: "elevation" \| "outlined"`, `elevation`, `raised`; CardHeader: `title`, `subheader`, `avatar`, `action`. **If the card is clickable, wrap its content in `CardActionArea`** (see below) | — |
94
96
  | `Paper` | `import { Paper } from "@matteoaliano/forest-ui"` | `variant: "elevation" \| "outlined"`, `elevation` (0-24), `square` | — |
95
- | `CardActionArea` | `import { CardActionArea } from "@matteoaliano/forest-ui"` | `onClick`, `href`, `disabled` |
97
+ | `CardActionArea` | `import { CardActionArea } from "@matteoaliano/forest-ui"` | `onClick`, `href`, `disabled`. **REQUIRED for any clickable card** — carries the faux-3D bevel + glaze + press affordance (same interaction language as Button) that signals a card is interactive. Never put `onClick` on a bare `Card`. | `<Card><CardActionArea onClick={…}><CardContent>…</CardContent></CardActionArea></Card>` |
96
98
  | `Drawer` | `import { Drawer } from "@matteoaliano/forest-ui"` | `open`, `onClose`, `anchor: "left" \| "right" \| "top" \| "bottom"`, `variant: "permanent" \| "persistent" \| "temporary"`, `hideBackdrop` | `anchor="right"`, `hideBackdrop={true}` |
97
99
  | `SwipeableDrawer` | `import { SwipeableDrawer } from "@matteoaliano/forest-ui"` | `onOpen` (required), `onClose` (required), `open`, `anchor`, `swipeAreaWidth` |
98
100
 
@@ -1,5 +1,30 @@
1
1
  # Forest UI — Code Patterns & Anti-Patterns
2
2
 
3
+ ## Setup details (Next.js, fonts, dark mode)
4
+
5
+ **Next.js config** — required so the bundler resolves the CSS subpath exports:
6
+
7
+ ```ts
8
+ // next.config.ts
9
+ const nextConfig = {
10
+ transpilePackages: ["@matteoaliano/forest-ui"],
11
+ experimental: {
12
+ optimizePackageImports: ["@matteoaliano/forest-ui", "@mui/material", "@mui/icons-material"],
13
+ },
14
+ };
15
+ ```
16
+
17
+ **Font verification** — after `next dev`, DevTools → Network (filter "font") must show `aeonik-*.woff2` loading `200 OK`, and a `<Typography>`'s computed font-family must read `"Aeonik", sans-serif` (Chrome → Rendered Fonts should say "Web font", not Arial/Helvetica). 404s on woff2 = missing `transpilePackages`. The AlkemyBETA family name is **one word** — `"Alkemy Beta"` will not resolve.
18
+
19
+ **Dark mode wiring:**
20
+
21
+ - Initial scheme: `<ForestProvider preset="forest-alkemy+" defaultMode="light" | "dark" | "system">`.
22
+ - Programmatic: `const { mode, setMode } = useColorScheme()` (re-exported). MUI owns a `.dark` class on `<html>` — never toggle classes yourself, or portaled Modal/Menu/Tooltip content stays light.
23
+ - Next.js App Router: render `<InitColorSchemeScript attribute="class" />` (re-exported) as the first child of `<body>` to avoid a flash of the wrong scheme.
24
+ - The visible switch is built into `<AppShell>` — never mount a second toggle.
25
+
26
+ **If a theme-only component renders like stock grey MUI** (Switch, Checkbox, Radio, ToggleButton): it was imported from `@mui/material` instead of `@matteoaliano/forest-ui`, or it's rendering outside `<ForestProvider>` (watch portals and nested non-Forest `ThemeProvider`s).
27
+
3
28
  ## Code Patterns
4
29
 
5
30
  ### Form Layout
@@ -134,94 +159,61 @@ function ConfirmDialog({ open, onClose, onConfirm }) {
134
159
  }
135
160
  ```
136
161
 
137
- ### App Shell (AppBar + Sidebar Navigation)
162
+ ### App Shell + Page (MANDATORY layout)
163
+
164
+ **Every authenticated surface renders inside `<AppShell>`, and every routed page inside `<Page>`.** Both are real components — do not hand-roll app chrome or page scaffolds.
138
165
 
139
- The standard application layout: a full-width AppBar at the top with a SidebarNav below it. The Toolbar uses three equal `flex: 1` columns so the Search stays visually centered. The sidebar drawer uses `position: relative` to flow inside the layout instead of overlaying as a fixed panel.
166
+ - `<AppShell>` owns the sticky AppBar (brand / centered search / actions), the SidebarNav, responsive mobile drawer, and the **color-mode toggle (built in never add your own)**. The brand auto-flips to negative artwork in dark mode.
167
+ - `<Page>` owns the flat `background.paper` surface, the 24px gutters, breadcrumbs, the page title, and header actions.
168
+ - **Every widget, chart, or table inside a `<Page>` must live in a `<Card>`.**
140
169
 
141
170
  ```tsx
142
- import { useState } from "react";
143
171
  import {
144
- AppBar, Toolbar, Logo, Search, IconButton, Badge,
145
- SidebarNav, SidebarItem, List, Typography, Box, Avatar,
172
+ AppShell, Page, Card, CardContent, IconButton, Badge, Avatar,
173
+ type AppShellNavItem,
146
174
  } from "@matteoaliano/forest-ui";
147
- import NotificationsOutlined from "@mui/icons-material/NotificationsOutlined";
148
- import SettingsOutlined from "@mui/icons-material/SettingsOutlined";
149
175
  import HomeOutlined from "@mui/icons-material/HomeOutlined";
150
176
  import BarChartOutlined from "@mui/icons-material/BarChartOutlined";
151
- import PeopleOutlined from "@mui/icons-material/PeopleOutlined";
177
+ import NotificationsOutlined from "@mui/icons-material/NotificationsOutlined";
152
178
 
153
- function AppShell({ children }) {
154
- const [sidebarOpen, setSidebarOpen] = useState(true);
179
+ const navItems: AppShellNavItem[] = [
180
+ // component/href/onClick are the router escape — plug in Next Link, React
181
+ // Router Link, or a plain onClick. `active` drives the selected state.
182
+ { label: "Home", icon: <HomeOutlined />, href: "/", active: true },
183
+ { label: "Analytics", icon: <BarChartOutlined />, href: "/analytics" },
184
+ ];
155
185
 
186
+ export function Layout() {
156
187
  return (
157
- <Box sx={{ display: "flex", flexDirection: "column", height: "100vh" }}>
158
- {/* ── App Bar ── */}
159
- <AppBar
160
- position="sticky"
161
- elevation={0}
162
- sx={{ zIndex: (theme) => theme.zIndex.drawer + 1 }}
163
- >
164
- <Toolbar variant="dense" sx={{ px: { xs: "16px", sm: "16px" } }}>
165
- {/* Left — Logo */}
166
- <Box sx={{ flex: 1, display: "flex", alignItems: "center" }}>
167
- <Logo product="wsuite" />
168
- </Box>
169
- {/* Center — Search (always visually centered) */}
170
- <Box sx={{ flex: 1, display: "flex", justifyContent: "center" }}>
171
- <Search size="small" sx={{ width: "100%", maxWidth: 480 }} />
172
- </Box>
173
- {/* Right Actions */}
174
- <Box sx={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "flex-end", gap: 1 }}>
175
- <IconButton size="small" color="inherit">
176
- <Badge color="error" variant="dot">
177
- <NotificationsOutlined fontSize="small" />
178
- </Badge>
179
- </IconButton>
180
- <IconButton size="small" color="inherit">
181
- <SettingsOutlined fontSize="small" />
182
- </IconButton>
183
- <Avatar sx={{ width: 28, height: 28, fontSize: 13 }}>MA</Avatar>
184
- </Box>
185
- </Toolbar>
186
- </AppBar>
187
-
188
- {/* ── Body: Sidebar + Content ── */}
189
- <Box sx={{ display: "flex", flexGrow: 1, overflow: "hidden" }}>
190
- <SidebarNav
191
- open={sidebarOpen}
192
- onOpenChange={setSidebarOpen}
193
- sx={{
194
- height: "100%",
195
- "& .MuiDrawer-paper": { position: "relative", height: "100%" },
196
- }}
197
- >
198
- <List>
199
- <SidebarItem icon={<HomeOutlined />} label="Home" selected />
200
- <SidebarItem icon={<BarChartOutlined />} label="Analytics" />
201
- <SidebarItem icon={<PeopleOutlined />} label="Users" />
202
- <SidebarItem icon={<SettingsOutlined />} label="Settings" />
203
- </List>
204
- </SidebarNav>
205
-
206
- <Box
207
- component="main"
208
- sx={{ flexGrow: 1, p: 3, overflow: "auto", backgroundColor: "background.paper" }}
209
- >
210
- {children}
211
- </Box>
212
- </Box>
213
- </Box>
188
+ <AppShell
189
+ navItems={navItems}
190
+ search // true = default centered Search; or pass a node
191
+ actions={
192
+ <>
193
+ <IconButton size="small" color="inherit">
194
+ <Badge badgeContent={3} color="error" variant="dot">
195
+ <NotificationsOutlined fontSize="small" />
196
+ </Badge>
197
+ </IconButton>
198
+ <Avatar sx={{ width: 28, height: 28, fontSize: 13 }}>MA</Avatar>
199
+ </>
200
+ }
201
+ >
202
+ <Page title="Home" breadcrumbs={[{ label: "Home" }]}>
203
+ <Card>
204
+ <CardContent>{/* every widget/chart/table goes in a Card */}</CardContent>
205
+ </Card>
206
+ </Page>
207
+ </AppShell>
214
208
  );
215
209
  }
216
210
  ```
217
211
 
218
- **Key patterns:**
219
- - `Toolbar variant="dense"` 48px height instead of 64px
220
- - Three `flex: 1` columns in Toolbar keeps Search centered regardless of left/right content width
221
- - `px: { xs: "16px", sm: "16px" }` on Toolbar — aligns logo with sidebar icons (overrides MUI's responsive 24px default)
222
- - `position: "relative"` on drawer papermakes sidebar flow in layout, not overlay
223
- - `height: "100%"` on SidebarNav — sidebar border extends full height
224
- - `backgroundColor: "background.paper"` on content — contrasts with sidebar/AppBar background
212
+ **Key props:**
213
+ - `AppShell`: `navItems` (data + `component`/`href`/`onClick` router escape, `endAdornment` for count chips), `nav` (full custom sidebar), `logoVariant` (`"logomark"` default | `"full"`), `brand` (custom node), `search` (bool/node), `actions`, `sidebarBehavior` (`"permanent"` | `"hover"`), `defaultSidebarOpen`.
214
+ - `Page`: `title` (required), `breadcrumbs` (data + `component` router escape; last crumb renders as plain text), `actions`, `disableGutters`, `maxWidth`.
215
+
216
+ The color-mode toggle is rendered by `<AppShell>` itself and is not removable never mount a second theme switch. See the **Patterns/App Shell** and **Patterns/Page** Storybook entries for live variants.
225
217
 
226
218
  ### Detail Drawer
227
219
 
@@ -6,10 +6,9 @@ How to keep `@matteoaliano/forest-ui` and its peer dependencies up to date.
6
6
 
7
7
  ```bash
8
8
  npm update @matteoaliano/forest-ui # bump within the semver range
9
- npx @matteoaliano/forest-ui # re-sync the Claude Code skill
10
9
  ```
11
10
 
12
- That's it for routine minor/patch updates. For majors, read the changelog first.
11
+ That's it for routine minor/patch updates. The Claude Code skill + enforcement hook update through the Forest Design System plugin — no re-sync step. For majors, read the changelog first.
13
12
 
14
13
  ## Check the installed version
15
14
 
@@ -53,27 +52,11 @@ npm install @matteoaliano/forest-ui@latest
53
52
 
54
53
  Commit your lockfile before upgrading so a rollback is one `git checkout` away.
55
54
 
56
- ## Re-sync the Claude Code skill
55
+ ## Keep the Claude Code plugin current
57
56
 
58
- The skill ships **inside** the package and evolves with it. After every upgrade:
57
+ AI guidance ships as the Forest Design System **plugin** (skill + enforcement hook), not as files copied into your repo. Update it through Claude Code's plugin system — there's no per-upgrade re-sync step, and nothing to commit into `.claude/`.
59
58
 
60
- ```bash
61
- npx @matteoaliano/forest-ui
62
- ```
63
-
64
- This refreshes `.claude/skills/forest-alkemy-plus/` with the latest component list, prop signatures, and patterns. **Skipping this step means Claude is working from stale rules** — it may recommend components that no longer exist or miss new ones.
65
-
66
- Automate it by wiring `postinstall`:
67
-
68
- ```json
69
- {
70
- "scripts": {
71
- "postinstall": "forest-ui"
72
- }
73
- }
74
- ```
75
-
76
- Commit `.claude/skills/` so every teammate (and CI) gets the same snapshot.
59
+ If you still have a `.claude/skills/forest-alkemy-plus/` folder from the old `npx forest-ui sync` flow, delete it — the plugin replaces it, and a stale copy competes with it.
77
60
 
78
61
  ## Peer dependencies
79
62
 
@@ -102,8 +85,7 @@ Quick checklist after bumping:
102
85
  - [ ] No runtime console errors about missing theme tokens
103
86
  - [ ] `<ForestProvider>` still wraps the app root
104
87
  - [ ] Fonts still render (Aeonik / Aeonik Mono / Alkemy BETA)
105
- - [ ] Visual spot-check on one page per preset in use
106
- - [ ] `.claude/skills/forest-alkemy-plus/SKILL.md` `version` frontmatter matches the new package version
88
+ - [ ] Visual spot-check on a representative page
107
89
 
108
90
  ## Rollback
109
91
 
@@ -112,7 +94,6 @@ If something breaks and you need to revert:
112
94
  ```bash
113
95
  git checkout -- package.json package-lock.json
114
96
  npm install
115
- npx @matteoaliano/forest-ui # re-sync to restore older skill
116
97
  ```
117
98
 
118
99
  Or pin to a known-good version explicitly:
@@ -130,7 +111,7 @@ npm install @matteoaliano/forest-ui@0.5.2
130
111
 
131
112
  ## Never do this on upgrade
132
113
 
133
- - Don't skip `npx @matteoaliano/forest-ui`stale skills are worse than no skills
114
+ - Don't keep a stale `.claude/skills/forest-alkemy-plus/` copy alongside the plugin it competes with the current rules
134
115
  - Don't upgrade MUI separately from forest-ui — they're paired
135
116
  - Don't mix two forest-ui majors in a monorepo without a shared hoist
136
117
  - Don't suppress peer-dep warnings — they're the early signal for theme breakage