---
description: Gentelella v4 — project conventions, recipes, anti-patterns
globs:
- "**/*.js"
- "**/*.scss"
- "**/*.html"
- "**/*.css"
alwaysApply: true
---
# Gentelella v4 (`4.0.0`)
Admin dashboard template by Colorlib. 58 server-rendered HTML pages in [production/](mdc:production), built with **Vite 8** (Rolldown). **Vanilla ES2022**, no Bootstrap, no jQuery, no SPA framework. SCSS only. Heavyweight deps (**ECharts 6**, **DataTables.net 2**, **Leaflet 1.9**) are lazy-imported per page.
Full reference: [CLAUDE.md](mdc:CLAUDE.md). The rules below are the load-bearing ones — apply them by default.
## Hard rules
- **Vanilla DOM only.** `querySelector`, `classList`, `addEventListener`. Never add jQuery, Bootstrap JS, or an SPA framework.
- **Single entry**: [src/main-v4.js](mdc:src/main-v4.js). Every page loads it. Page-specific modules are lazy-imported inside `main-v4.js` guarded by DOM presence — match that pattern, don't add new ``. Then append a leaf to `NAV` in [src/v4/shell-render.js](mdc:src/v4/shell-render.js) (`key` matches `data-page`).
## Recipe: new chart
1. Markup: `
` in the page.
2. `case '':` in `initCharts()` in [src/v4/charts.js](mdc:src/v4/charts.js) returning the ECharts `option`.
3. Read tokens via `getComputedStyle(document.documentElement).getPropertyValue('--token')` for colors.
## Recipe: new page module
In `src/main-v4.js`, add:
```js
if (document.querySelector('.reports-root')) {
import('./v4/reports.js').then((m) => m.initReports());
}
```
Export a single idempotent `initReports()` from `src/v4/reports.js`.
## Recipe: modal / toast
```js
import { showModal } from './v4/modal.js';
showModal({
title: 'Delete project?',
body: 'This cannot be undone.',
actions: [
{ label: 'Cancel', variant: 'ghost' },
{ label: 'Delete', variant: 'danger', action: () => { /* … */ } }
]
});
import { showToast } from './v4/toast.js';
showToast('Saved', { variant: 'success' });
```
## Commands
```bash
npm run dev # Vite dev server on :9173
npm run build # Production build → dist/
npm run preview # Serve built dist/ on :9174
npm run lint # ESLint over src/
npm run format # Prettier write
npm run new -- # Scaffold a page
npm run screenshots # Playwright capture (22 pages × light+dark)
npm run smoke # Hit every page, assert 200
npm run analyze # Build + open dist/stats.html
npm run deploy:preview # Build + R2 sync
```
Override the dev port with `PORT=…`. Build under a subpath with `BASE_PATH=/foo/ npm run build`.