@matteoaliano/forest-ui 1.0.0 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matteoaliano/forest-ui",
3
- "version": "1.0.0",
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,158 +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: 1.0.0
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.
20
- 8. **A clickable card MUST use `CardActionArea`.** Wrap the card's content in `<CardActionArea onClick={…}>` — never put `onClick`/`href` on a bare `<Card>`. `CardActionArea` carries the faux-3D bevel + glaze + press affordance (the same interaction language as `Button`), which is how users tell a clickable card apart from a static one. Static (non-interactive) cards must **not** use it.
21
- 9. **Every authenticated product surface MUST use the App Shell layout.** Build the top-level chrome from the App Shell pattern (AppBar + SidebarNav) — see Setup → App Shell and `references/patterns.md`. Its AppBar actions MUST include the color-mode toggle (`ColorModeToggle`), which is the single sanctioned dark-mode switch. Do not hand-roll a bespoke app frame or scatter ad-hoc theme toggles elsewhere.
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.
22
19
 
23
- ## Setup
24
-
25
- ### Step 1 — Install
26
-
27
- ```bash
28
- npm install @matteoaliano/forest-ui \
29
- @mui/material@^7 @mui/icons-material@^7 \
30
- @mui/x-charts@^7 @mui/x-data-grid@^7 @mui/x-date-pickers@^8 \
31
- @emotion/react @emotion/styled dayjs
32
- ```
33
-
34
- > **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"`).
35
-
36
- ### Step 2 — Load the fonts (CRITICAL — do this before anything else)
37
-
38
- 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)
39
21
 
40
22
  ```tsx
41
- // Next.js App Router app/layout.tsx (above any other CSS imports)
42
- // Next.js Pages Router → pages/_app.tsx
43
- // Vite / CRA → src/main.tsx or src/index.tsx
23
+ // App entry fonts first, then the provider:
44
24
  import "@matteoaliano/forest-ui/fonts/aeonik/aeonik.css";
45
25
  import "@matteoaliano/forest-ui/fonts/aeonik-mono/aeonik-mono.css";
46
26
  import "@matteoaliano/forest-ui/fonts/alkemy-beta/alkemy-beta.css";
47
- ```
48
-
49
- No `public/fonts/` copy, no `next/font/local`, no manual `@font-face` rules — the bundler resolves the relative `.woff2` URLs inside the CSS automatically.
50
-
51
- **For Next.js**, also add `transpilePackages` so Turbopack resolves the CSS subpath exports cleanly:
27
+ import { ForestProvider } from "@matteoaliano/forest-ui";
52
28
 
53
- ```ts
54
- // next.config.ts
55
- const nextConfig = {
56
- transpilePackages: ["@matteoaliano/forest-ui"],
57
- experimental: {
58
- optimizePackageImports: [
59
- "@matteoaliano/forest-ui",
60
- "@mui/material",
61
- "@mui/icons-material",
62
- ],
63
- },
64
- };
65
- export default nextConfig;
29
+ <ForestProvider preset="forest-alkemy+" defaultMode="system">{app}</ForestProvider>
66
30
  ```
67
31
 
68
- **Verify it worked.** Open DevTools after running `next dev`:
69
-
70
- 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).
71
- 2. **Elements → Computed → font-family** on a `<Typography>` — should read `"Aeonik", sans-serif`.
72
- 3. **Elements → Rendered Fonts** (Chrome only) — should say `Aeonik — Web font`, not `Arial — Local file` or `Helvetica — Local file`.
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
- 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.
75
-
76
- ### Step 3 — Wrap your app
34
+ ## Layout baseline (mandatory shape of every screen)
77
35
 
78
36
  ```tsx
79
- import { ForestProvider } from "@matteoaliano/forest-ui";
80
-
81
- function App() {
82
- return (
83
- <ForestProvider preset="forest-alkemy+">
84
- {/* All app content here */}
85
- </ForestProvider>
86
- );
87
- }
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>
88
42
  ```
89
43
 
90
- ## Dark Mode
91
-
92
- The preset ships a dark color scheme (MUI `colorSchemes` / CSS variables). It
93
- activates on a **`.dark` class** on any ancestor — same convention as
94
- `@matteoaliano/forest-web`.
95
-
96
- - Class toggle: put `dark` on `<html>` (`document.documentElement.classList.toggle("dark", isDark)`) — NOT a deep wrapper, or portaled components (Modal/Menu/Tooltip) stay light. `useColorScheme` handles this for you.
97
- - Initial scheme: `<ForestProvider preset="forest-alkemy+" defaultMode="system">` — `defaultMode` is `"light"` (default), `"dark"`, or `"system"` (follows `prefers-color-scheme`).
98
- - Programmatic: `const { mode, setMode } = useColorScheme();` (re-exported from the package) — call `setMode("dark" | "light" | "system")`.
99
- - Next.js App Router: render `<InitColorSchemeScript attribute="class" />` (re-exported) as the first child of `<body>` to avoid a flash of the wrong scheme.
100
- - **Where the switch lives:** expose it as the `ColorModeToggle` in the App Shell's AppBar actions (see `references/patterns.md` → App Shell). That is the sanctioned, single entry point for toggling dark mode — don't duplicate it.
101
-
102
- Do NOT hand-roll a second theme or a `.dark` stylesheet — the palette already
103
- inverts through the preset's dark tokens; just toggle the class or use `setMode`.
104
-
105
- ## Typography & Fonts
106
-
107
- - **Aeonik** — the default font for all UI text (headings, body, labels, buttons, etc.). Family name: `"Aeonik"`.
108
- - **Aeonik Mono** — use for numeric values: prices, stats, table figures, counters, dates, IDs, code snippets. Family name: `"Aeonik Mono"`.
109
- - **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:
110
- ```tsx
111
- <Typography variant="h1" sx={{ fontFamily: '"AlkemyBETA", serif' }}>...</Typography>
112
- ```
113
-
114
- 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.
115
-
116
- ## Available Components
117
-
118
- **Inputs:** Button, ButtonGroup, TextField, Select + MenuItem, MultiSelect, Checkbox, RadioGroup + Radio, Switch, ToggleButton + ToggleButtonGroup, Fab, IconButton, Autocomplete, Search, DatePicker, Input
119
-
120
- **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
121
-
122
- **Surfaces:** Accordion family, AppBar + Toolbar + AppBarNavItem, Card family + CardActionArea, Paper, Drawer + SwipeableDrawer
123
-
124
- **Feedback:** Alert + AlertTitle, Dialog family, Backdrop, LinearProgress, CircularProgress, Skeleton, Modal, Popover, Snackbar + SnackbarContent, Slider
125
-
126
- **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
127
-
128
- **Layout:** Box, Stack, Grid, Container
129
-
130
- **Form Helpers:** FormControl, FormControlLabel, FormGroup, FormHelperText, FormLabel, InputAdornment, InputBase, InputLabel, OutlinedInput, FilledInput
131
-
132
- **Transitions:** Collapse, Fade, Grow, Slide, Zoom
133
-
134
- **Utilities:** ClickAwayListener, NoSsr, Portal, Popper, TextareaAutosize, SvgIcon, ButtonBase, GlobalStyles
135
-
136
- **Hooks:** useMediaQuery, useScrollTrigger, useFormControl, useTheme
137
-
138
- **Charts:** BarChart, LineChart, PieChart, ScatterChart, Gauge, SparkLineChart, useChartColors, plus composition primitives (see components.md)
139
-
140
- 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".
141
45
 
142
- ## Tooltip vs Popover
46
+ ## Fonts
143
47
 
144
- - **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.
145
- - **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' }}`.
146
49
 
147
- **Rule of thumb:** If the overlay content is just a string, use `<Tooltip>`. If it contains components, use `<Popover>`.
50
+ ## Components
148
51
 
149
- ## 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).
150
53
 
151
- 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.
152
- 2. **Writing the AlkemyBETA family with a space** (`"Alkemy Beta"`) — the `@font-face` declares `"AlkemyBETA"` (one word). The spaced form fails to resolve.
153
- 3. **Importing from `@mui/material`** instead of `@matteoaliano/forest-ui` — all components, hooks, and layout primitives are available from forest-ui
154
- 4. **Hardcoded color values** (`backgroundColor: "#7f56d9"`) instead of theme tokens (`backgroundColor: "primary.main"`)
155
- 5. **Hardcoded spacing** (`padding: "16px"`) instead of theme spacing (`p: 4`)
156
- 6. **Missing `<ForestProvider>`** at the app root — theme won't apply
157
- 7. **Using filled MUI icons** (`Close`) instead of Outlined (`CloseOutlined`)
158
- 8. **Building custom multi-select** instead of using the `<MultiSelect>` component
159
- 9. **`onClick` on a bare `<Card>`** — a clickable card must wrap its content in `<CardActionArea>` so it gets the 3D bevel/glaze/press affordance. `<Card onClick>` looks identical to a static card and gives no click cue.
160
- 10. **Theme-only components rendering like stock MUI grey** — `Switch`, `Checkbox`, `Radio`, `ToggleButton` (and other pure re-exports) carry NO styling of their own; 100% of their Forest look comes from the theme applied by `<ForestProvider>`. If one looks unstyled/plain-grey while the rest of the app looks right: (a) confirm you imported it from `@matteoaliano/forest-ui`, **not** `@mui/material` (a stray `import Switch from "@mui/material/Switch"` is the usual culprit — it renders with zero error), and (b) confirm it's actually inside `<ForestProvider>` (watch portals/modals and any nested non-Forest MUI `ThemeProvider`, which resets the overrides for its subtree).
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` |
@@ -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,142 +159,61 @@ function ConfirmDialog({ open, onClose, onConfirm }) {
134
159
  }
135
160
  ```
136
161
 
137
- ### App Shell (AppBar + Sidebar Navigation) — MANDATORY layout
162
+ ### App Shell + Page (MANDATORY layout)
138
163
 
139
- **This is the required top-level layout for every authenticated Forest UI product surface.** Do not hand-roll a bespoke app chrome per project start from this shell so brand, spacing, sidebar behavior, and the dark-mode toggle are consistent across all Forest apps. A full-width AppBar sits 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.
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.
140
165
 
141
- Two requirements are non-negotiable:
142
-
143
- 1. **The AppBar actions MUST include the color-mode toggle** (`ColorModeToggle` below, backed by `useColorScheme`). This is the single sanctioned dark-mode switch — do not scatter ad-hoc toggles elsewhere.
144
- 2. **The brand is always the logomark**, and it flips to `color="negative"` when dark mode is active so it stays legible on the dark AppBar.
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>`.**
145
169
 
146
170
  ```tsx
147
- import { useState } from "react";
148
171
  import {
149
- AppBar, Toolbar, Logo, Search, IconButton, Badge, Tooltip,
150
- SidebarNav, SidebarItem, List, Typography, Box, Avatar,
151
- useColorScheme,
172
+ AppShell, Page, Card, CardContent, IconButton, Badge, Avatar,
173
+ type AppShellNavItem,
152
174
  } from "@matteoaliano/forest-ui";
153
- import NotificationsOutlined from "@mui/icons-material/NotificationsOutlined";
154
- import SettingsOutlined from "@mui/icons-material/SettingsOutlined";
155
- import DarkModeOutlined from "@mui/icons-material/DarkModeOutlined";
156
- import LightModeOutlined from "@mui/icons-material/LightModeOutlined";
157
175
  import HomeOutlined from "@mui/icons-material/HomeOutlined";
158
176
  import BarChartOutlined from "@mui/icons-material/BarChartOutlined";
159
- import PeopleOutlined from "@mui/icons-material/PeopleOutlined";
160
-
161
- // Brand: always the logomark, negative artwork in dark mode.
162
- function AppShellLogo() {
163
- const { mode, systemMode } = useColorScheme();
164
- const resolved = mode === "system" ? systemMode : mode;
165
- return (
166
- <Logo
167
- product="studio"
168
- variant="logomark"
169
- color={resolved === "dark" ? "negative" : "default"}
170
- />
171
- );
172
- }
173
-
174
- // Mandatory light/dark switch. `useColorScheme` toggles the `.dark` class on
175
- // <html> for you — the preset's dark tokens invert the whole palette.
176
- function ColorModeToggle() {
177
- const { mode, systemMode, setMode } = useColorScheme();
178
- const resolved = mode === "system" ? systemMode : mode;
179
- const isDark = resolved === "dark";
180
- return (
181
- <Tooltip title={isDark ? "Switch to light mode" : "Switch to dark mode"}>
182
- <IconButton
183
- size="small"
184
- color="inherit"
185
- aria-label="Toggle color mode"
186
- onClick={() => setMode(isDark ? "light" : "dark")}
187
- >
188
- {isDark ? <LightModeOutlined fontSize="small" /> : <DarkModeOutlined fontSize="small" />}
189
- </IconButton>
190
- </Tooltip>
191
- );
192
- }
177
+ import NotificationsOutlined from "@mui/icons-material/NotificationsOutlined";
193
178
 
194
- function AppShell({ children }) {
195
- 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
+ ];
196
185
 
186
+ export function Layout() {
197
187
  return (
198
- <Box sx={{ display: "flex", flexDirection: "column", height: "100vh" }}>
199
- {/* ── App Bar ── */}
200
- <AppBar
201
- position="sticky"
202
- elevation={0}
203
- sx={{ zIndex: (theme) => theme.zIndex.drawer + 1 }}
204
- >
205
- <Toolbar variant="dense" sx={{ px: { xs: "16px", sm: "16px" } }}>
206
- {/* Left — Logo (centered over the 48px sidebar rail) */}
207
- <Box sx={{ flex: 1, display: "flex", alignItems: "center" }}>
208
- <Box sx={{ width: 48, ml: "-16px", display: "flex", justifyContent: "center" }}>
209
- <AppShellLogo />
210
- </Box>
211
- </Box>
212
- {/* Center Search (always visually centered) */}
213
- <Box sx={{ flex: 1, display: "flex", justifyContent: "center" }}>
214
- <Search size="small" sx={{ width: "100%", maxWidth: 480 }} />
215
- </Box>
216
- {/* Right — Actions (ColorModeToggle is mandatory) */}
217
- <Box sx={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "flex-end", gap: 1 }}>
218
- <ColorModeToggle />
219
- <IconButton size="small" color="inherit">
220
- <Badge color="error" variant="dot">
221
- <NotificationsOutlined fontSize="small" />
222
- </Badge>
223
- </IconButton>
224
- <IconButton size="small" color="inherit">
225
- <SettingsOutlined fontSize="small" />
226
- </IconButton>
227
- <Avatar sx={{ width: 28, height: 28, fontSize: 13 }}>MA</Avatar>
228
- </Box>
229
- </Toolbar>
230
- </AppBar>
231
-
232
- {/* ── Body: Sidebar + Content ── */}
233
- <Box sx={{ display: "flex", flexGrow: 1, overflow: "hidden" }}>
234
- <SidebarNav
235
- open={sidebarOpen}
236
- onOpenChange={setSidebarOpen}
237
- sx={{
238
- height: "100%",
239
- "& .MuiDrawer-paper": { position: "relative", height: "100%" },
240
- }}
241
- >
242
- <List>
243
- <SidebarItem icon={<HomeOutlined />} label="Home" selected />
244
- <SidebarItem icon={<BarChartOutlined />} label="Analytics" />
245
- <SidebarItem icon={<PeopleOutlined />} label="Users" />
246
- <SidebarItem icon={<SettingsOutlined />} label="Settings" />
247
- </List>
248
- </SidebarNav>
249
-
250
- <Box
251
- component="main"
252
- sx={{ flexGrow: 1, p: 3, overflow: "auto", backgroundColor: "background.paper" }}
253
- >
254
- {children}
255
- </Box>
256
- </Box>
257
- </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>
258
208
  );
259
209
  }
260
210
  ```
261
211
 
262
- **Key patterns:**
263
- - `ColorModeToggle` in AppBar actions mandatory, single sanctioned dark-mode switch
264
- - Logomark flips to `color="negative"` in dark mode brand stays legible on the dark AppBar
265
- - `Toolbar variant="dense"` — 48px height instead of 64px
266
- - Three `flex: 1` columns in Toolbarkeeps Search centered regardless of left/right content width
267
- - `px: { xs: "16px", sm: "16px" }` on Toolbar — aligns logo with sidebar icons (overrides MUI's responsive 24px default)
268
- - `position: "relative"` on drawer paper — makes sidebar flow in layout, not overlay
269
- - `height: "100%"` on SidebarNav — sidebar border extends full height
270
- - `backgroundColor: "background.paper"` on content — contrasts with sidebar/AppBar background
271
-
272
- See the **Patterns/App Shell** Storybook entry for the canonical, runnable version (Default / CollapsedSidebar / HoverSidebar / StudioBrand).
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.
273
217
 
274
218
  ### Detail Drawer
275
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