@matteoaliano/forest-ui 0.8.8 → 1.0.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-I3T6PQLZ.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.8",
3
+ "version": "1.0.0",
4
4
  "description": "Forest Design System — themed MUI components",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -3,7 +3,7 @@ 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.8
6
+ version: 1.0.0
7
7
  ---
8
8
 
9
9
  # Forest UI — Alkemy+ Preset
@@ -17,6 +17,8 @@ metadata:
17
17
  5. **NEVER create custom component wrappers** for things Forest UI already provides.
18
18
  6. **TypeScript is required.** All components export their prop types (e.g. `type ButtonProps`).
19
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.
20
22
 
21
23
  ## Setup
22
24
 
@@ -85,6 +87,21 @@ function App() {
85
87
  }
86
88
  ```
87
89
 
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
+
88
105
  ## Typography & Fonts
89
106
 
90
107
  - **Aeonik** — the default font for all UI text (headings, body, labels, buttons, etc.). Family name: `"Aeonik"`.
@@ -139,3 +156,5 @@ See `references/components.md` for full API details, `references/patterns.md` fo
139
156
  6. **Missing `<ForestProvider>`** at the app root — theme won't apply
140
157
  7. **Using filled MUI icons** (`Close`) instead of Outlined (`CloseOutlined`)
141
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).
@@ -52,7 +52,7 @@ import Button from "@mui/material/Button";
52
52
  | `Switch` | `import { Switch } from "@matteoaliano/forest-ui"` | `color: "default" \| "primary" \| "secondary" \| "error" \| "info" \| "success" \| "warning"`, `size: "small" \| "medium"`, `checked`, `disabled` | — |
53
53
  | `ToggleButton` + `ToggleButtonGroup` | `import { ToggleButton, ToggleButtonGroup } from "@matteoaliano/forest-ui"` | `value`, `exclusive`, `onChange`, `orientation: "horizontal" \| "vertical"`, `size: "small" \| "medium" \| "large"`, `color`, `fullWidth` | — |
54
54
  | `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` | |
55
+ | `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
56
  | `Autocomplete` | `import { Autocomplete } from "@matteoaliano/forest-ui"` | `options`, `renderInput` (required), `multiple`, `freeSolo`, `disableClearable`, `loading`, `groupBy`, `limitTags`, `filterOptions` | — |
57
57
  | `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
58
  | `Input` | `import { Input } from "@matteoaliano/forest-ui"` | `value`, `onChange`, `placeholder`, `multiline`, `rows`, `startAdornment`, `endAdornment`, `disabled`, `error` — all InputBase props | — |
@@ -90,9 +90,9 @@ import Button from "@mui/material/Button";
90
90
  | `Accordion` family | `import { Accordion, AccordionSummary, AccordionDetails, AccordionActions } from "@matteoaliano/forest-ui"` | `expanded`, `defaultExpanded`, `onChange`, `disabled`, `disableGutters`, `square` | — |
91
91
  | `AppBar` + `Toolbar` | `import { AppBar, Toolbar } from "@matteoaliano/forest-ui"` | `position`, `color`, `elevation`; Toolbar: `variant: "dense" \| "regular"` | — |
92
92
  | `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` | — |
93
+ | `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
94
  | `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` |
95
+ | `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
96
  | `Drawer` | `import { Drawer } from "@matteoaliano/forest-ui"` | `open`, `onClose`, `anchor: "left" \| "right" \| "top" \| "bottom"`, `variant: "permanent" \| "persistent" \| "temporary"`, `hideBackdrop` | `anchor="right"`, `hideBackdrop={true}` |
97
97
  | `SwipeableDrawer` | `import { SwipeableDrawer } from "@matteoaliano/forest-ui"` | `onOpen` (required), `onClose` (required), `open`, `anchor`, `swipeAreaWidth` |
98
98
 
@@ -134,22 +134,63 @@ function ConfirmDialog({ open, onClose, onConfirm }) {
134
134
  }
135
135
  ```
136
136
 
137
- ### App Shell (AppBar + Sidebar Navigation)
137
+ ### App Shell (AppBar + Sidebar Navigation) — MANDATORY layout
138
138
 
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.
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.
140
+
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.
140
145
 
141
146
  ```tsx
142
147
  import { useState } from "react";
143
148
  import {
144
- AppBar, Toolbar, Logo, Search, IconButton, Badge,
149
+ AppBar, Toolbar, Logo, Search, IconButton, Badge, Tooltip,
145
150
  SidebarNav, SidebarItem, List, Typography, Box, Avatar,
151
+ useColorScheme,
146
152
  } from "@matteoaliano/forest-ui";
147
153
  import NotificationsOutlined from "@mui/icons-material/NotificationsOutlined";
148
154
  import SettingsOutlined from "@mui/icons-material/SettingsOutlined";
155
+ import DarkModeOutlined from "@mui/icons-material/DarkModeOutlined";
156
+ import LightModeOutlined from "@mui/icons-material/LightModeOutlined";
149
157
  import HomeOutlined from "@mui/icons-material/HomeOutlined";
150
158
  import BarChartOutlined from "@mui/icons-material/BarChartOutlined";
151
159
  import PeopleOutlined from "@mui/icons-material/PeopleOutlined";
152
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
+ }
193
+
153
194
  function AppShell({ children }) {
154
195
  const [sidebarOpen, setSidebarOpen] = useState(true);
155
196
 
@@ -162,16 +203,19 @@ function AppShell({ children }) {
162
203
  sx={{ zIndex: (theme) => theme.zIndex.drawer + 1 }}
163
204
  >
164
205
  <Toolbar variant="dense" sx={{ px: { xs: "16px", sm: "16px" } }}>
165
- {/* Left — Logo */}
206
+ {/* Left — Logo (centered over the 48px sidebar rail) */}
166
207
  <Box sx={{ flex: 1, display: "flex", alignItems: "center" }}>
167
- <Logo product="wsuite" />
208
+ <Box sx={{ width: 48, ml: "-16px", display: "flex", justifyContent: "center" }}>
209
+ <AppShellLogo />
210
+ </Box>
168
211
  </Box>
169
212
  {/* Center — Search (always visually centered) */}
170
213
  <Box sx={{ flex: 1, display: "flex", justifyContent: "center" }}>
171
214
  <Search size="small" sx={{ width: "100%", maxWidth: 480 }} />
172
215
  </Box>
173
- {/* Right — Actions */}
216
+ {/* Right — Actions (ColorModeToggle is mandatory) */}
174
217
  <Box sx={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "flex-end", gap: 1 }}>
218
+ <ColorModeToggle />
175
219
  <IconButton size="small" color="inherit">
176
220
  <Badge color="error" variant="dot">
177
221
  <NotificationsOutlined fontSize="small" />
@@ -216,6 +260,8 @@ function AppShell({ children }) {
216
260
  ```
217
261
 
218
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
219
265
  - `Toolbar variant="dense"` — 48px height instead of 64px
220
266
  - Three `flex: 1` columns in Toolbar — keeps Search centered regardless of left/right content width
221
267
  - `px: { xs: "16px", sm: "16px" }` on Toolbar — aligns logo with sidebar icons (overrides MUI's responsive 24px default)
@@ -223,6 +269,8 @@ function AppShell({ children }) {
223
269
  - `height: "100%"` on SidebarNav — sidebar border extends full height
224
270
  - `backgroundColor: "background.paper"` on content — contrasts with sidebar/AppBar background
225
271
 
272
+ See the **Patterns/App Shell** Storybook entry for the canonical, runnable version (Default / CollapsedSidebar / HoverSidebar / StudioBrand).
273
+
226
274
  ### Detail Drawer
227
275
 
228
276
  Use the `Drawer` component when a user clicks an item (table row, card, list entry) to view or edit its details in a side panel. The Drawer defaults to `anchor="right"` and `hideBackdrop={true}`, so it opens on the right without dimming the page — the user retains full visibility of the content behind it.