@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/bin/sync.mjs +37 -72
- package/dist/index.d.mts +74 -2
- package/dist/index.d.ts +74 -2
- package/dist/index.js +250 -40
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +257 -49
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/skills/forest-alkemy-plus/SKILL.md +25 -131
- package/skills/forest-alkemy-plus/references/components.md +2 -0
- package/skills/forest-alkemy-plus/references/patterns.md +65 -121
- package/skills/forest-alkemy-plus/references/upgrading.md +6 -25
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matteoaliano/forest-ui",
|
|
3
|
-
"version": "1.
|
|
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.
|
|
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. **
|
|
14
|
-
2. **
|
|
15
|
-
3. **
|
|
16
|
-
4. **
|
|
17
|
-
5. **
|
|
18
|
-
6. **
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
### Step 3 — Wrap your app
|
|
34
|
+
## Layout baseline (mandatory shape of every screen)
|
|
77
35
|
|
|
78
36
|
```tsx
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
46
|
+
## Fonts
|
|
143
47
|
|
|
144
|
-
|
|
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
|
-
|
|
50
|
+
## Components
|
|
148
51
|
|
|
149
|
-
|
|
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
|
-
|
|
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
|
|
162
|
+
### App Shell + Page (MANDATORY layout)
|
|
138
163
|
|
|
139
|
-
**
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
150
|
-
|
|
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
|
|
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
|
-
|
|
195
|
-
|
|
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
|
-
<
|
|
199
|
-
{
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
|
263
|
-
- `
|
|
264
|
-
-
|
|
265
|
-
|
|
266
|
-
-
|
|
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
|
-
##
|
|
55
|
+
## Keep the Claude Code plugin current
|
|
57
56
|
|
|
58
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|