# CLAUDE.md
Guidance for Claude Code (claude.ai/code) when working in this repository. Cross-tool counterparts: [AGENTS.md](AGENTS.md), [.cursor/rules/project.mdc](.cursor/rules/project.mdc), [.github/copilot-instructions.md](.github/copilot-instructions.md). Each tool reads only its own file; content overlaps intentionally.
## What this is
Gentelella v4 (`4.1.1`) — free admin dashboard template by Colorlib. **58 production HTML pages** under [production/](production/), built with Vite 8 (Rolldown). Vanilla ES2022, no Bootstrap, no jQuery, no SPA framework. SCSS-only styling. ECharts 6, DataTables.net 3, and Leaflet 1.9 are the only heavyweight runtime deps — all lazy-imported per page.
Live preview: <https://preview.colorlib.com/theme/gentelella/>.
## Commands
```bash
npm run dev # Vite dev server on :9173, opens /production/index.html
npm run build # Production build → dist/
npm run preview # Serve built dist/ on :9174
npm run lint # ESLint over src/
npm run lint:fix # Auto-fix
npm run format # Prettier write
npm run format:check # Prettier check
npm run new -- <slug> # Scaffold a new page under production/
npm run screenshots # Playwright captures 22 pages × light+dark → docs/screenshots/
npm run smoke # Boot dev server, hit every page, assert 200
npm run analyze # Build + open dist/stats.html
npm run deploy:preview # Build + sync to R2 with per-file cache headers
```
Override the dev port via `PORT=…`; build under a subpath via `BASE_PATH=/foo/ npm run build`.
## Architecture
**Entry point**: [src/main-v4.js](src/main-v4.js) — single bundle for every page. Imports `scss/v4/main.scss`, mounts the shell, registers ECharts/DataTables/Leaflet placeholders, then lazy-imports page-specific modules guarded by DOM presence:
```js
if (document.getElementById('inbox-root')) {
import('./v4/inbox.js').then((m) => m.initInbox());
}
```
**Shell injection** ([vite.config.js](vite.config.js) `shellInjectionPlugin`): pages opt in by setting `<body data-shell="admin" data-page="key" data-breadcrumb="A > B">`. At build/dev time the Vite plugin inlines sidebar/topbar/footer HTML directly into the document so the shell paints on the first frame — no FOUC. Runtime [src/v4/shell.js](src/v4/shell.js) `mountShell()` is the fallback for raw-file viewing and always wires up event handlers (mobile drawer, theme toggle, sidebar accordion).
**Pages are auto-discovered** by `discoverEntries()` in [vite.config.js](vite.config.js): every `.html` file in `production/` becomes a Rollup input. Drop a file in, run dev, it's live — no config edit.
**Chunking**: only three vendor chunks are emitted, all lazy:
| Chunk | Loaded on | Source |
| ---------------- | ----------- | ------------------------------- |
| `vendor-echarts` | chart pages | `node_modules/echarts/` |
| `vendor-tables` | table pages | `node_modules/datatables.net/` |
| `vendor-maps` | map page | `node_modules/leaflet/` |
Everything else (shell, command palette, charts wrapper, tables wrapper, etc.) is in the main chunk and is small enough not to need splitting.
### Directory layout
```text
src/
├── main-v4.js # Entry — mounts shell, lazy-loads modules
├── scss/
│ ├── v4/
│ │ ├── main.scss # Entry — @use's the partials below
│ │ ├── _tokens.scss # CSS custom properties (light + dark)
│ │ ├── _layout.scss # Page wrapper, sidebar, topbar, grid
│ │ ├── _components.scss# Buttons, cards, badges, forms, …
│ │ ├── _widgets.scss # Stat cards, mini-charts, todo lists, …
│ │ ├── _forms.scss # Inputs, switches, date pickers
│ │ ├── _datatable.scss # DataTables re-skin
│ │ ├── _pages.scss # Per-page styles (kept narrow)
│ │ ├── _apps.scss # Inbox, kanban, chat, calendar, settings
│ │ └── _auth.scss # Login/register/forgot/2FA/lock/errors
└── v4/
├── shell.js # mountShell — sidebar/topbar wiring
├── shell-render.js # Pure renderers + NAV definition (used by Vite plugin)
├── menus.js # openMenu/openPanel dropdowns
├── modal.js # showModal
├── toast.js # showToast
├── charts.js # ECharts factory + initCharts()
├── tables.js # DataTables initialiser
├── command-palette.js # ⌘K
├── page-actions.js # Per-page action button delegation
├── inbox.js # Folders, reader, compose
├── kanban.js # Drag/drop board
├── calendar.js # FullCalendar-style CRUD
├── settings.js # localStorage-backed settings page
├── form-controls.js # Date range, multi-select, rich text
├── file-manager.js # Tree + grid file browser
├── details.js # Disclosure rows
├── markup.js # HTML pretty-printer for component playground
├── data-adapter.js # Demo data shim
├── product-images.js # E-commerce gallery
└── product-mockups.js # Storefront demo
production/ # 58 HTML entry pages (auto-discovered)
public/ # Static assets copied verbatim to dist/
types/gentelella.d.ts # TypeScript declarations for the public JS surface
scripts/
├── new-page.mjs # Scaffold a page + register in NAV
├── screenshots.mjs # Playwright capture (22 pages × 2 themes)
├── smoke.mjs # Boot dev server, fetch every page
└── deploy-preview.sh # Build + R2 sync + cache-header pass
examples/ # Standalone integration examples (Express/SQLite, etc.)
```
### Adding a new page
Use the scaffolder — it writes the HTML, sets the body attributes correctly, and (optionally) inserts the page into NAV:
```bash
npm run new -- reports --title "Reports" --nav-group "Admin"
npm run new -- user-roles --title "User roles" \
--breadcrumb "Home > User management|user_management.html > Roles" \
--nav-group "Admin" --icon profile
```
If you write the file by hand instead, the contract is:
1. Drop `production/<slug>.html`. Vite auto-discovers it (no config edit).
2. Set `<body data-shell="admin" data-page="<slug>" data-breadcrumb="Home > …">`.
3. Add a `<script type="module" src="/src/main-v4.js"></script>` in `<head>`.
4. To appear in the sidebar, edit `NAV` in [src/v4/shell-render.js](src/v4/shell-render.js) — match key to your `data-page`.
### NAV and icons
Single source of truth: `NAV` in [src/v4/shell-render.js](src/v4/shell-render.js). 7 groups (General, Apps, E-commerce, Projects, UI library, Admin, Layouts). Items are either flat leaves `{ key, href, text, icon, badge? }` or parents with a `children: []` array — the parent stays expanded if any child matches the page's `data-page`.
Icons are inline SVG strings in the `ICONS` object in the same file. Use a `data-page` whose `icon:` matches a key; add new icons by appending to `ICONS` (one SVG per entry, currentColor stroke).
### Breadcrumbs
`data-breadcrumb="Home > Forms > Advanced"` — split on `>`, rendered by `renderTopbar()` in [src/v4/shell-render.js](src/v4/shell-render.js). The last segment is the current page: never a link, always `aria-current="page"`. Every earlier segment resolves to a link in this order:
1. **Explicit target** — `data-breadcrumb="Home > Projects|projects.html > Acme Redesign"`. Everything after `|` is the href.
2. **NAV label match** — `CRUMB_HREFS` is built from `NAV` at module load, so a segment whose text exactly matches a nav item links to it. A parent group resolves to its first child. `Home` → `index.html` is the one hand-seeded entry.
3. **Neither** — plain text, no link.
Links are server-rendered by the Vite plugin along with the rest of the shell, so they work with JS disabled and never hydrate in after paint.
Prefer a crumb level that points somewhere. If a segment is a pure sidebar grouping with no landing page (`Apps`, `Layouts`, `Admin`), drop the level rather than shipping a dead crumb — `Home > Kanban`, not `Home > Apps > Kanban`. [production/level2.html](production/level2.html) is the deliberate exception; it demonstrates unlinked segments.
### Theming
Tokens in [src/scss/v4/_tokens.scss](src/scss/v4/_tokens.scss) — CSS custom properties under `:root` (light) and `[data-theme="dark"]`. The pre-paint inline script in `vite.config.js` reads `localStorage.getItem('theme')` and sets `data-theme` on `<html>` before body render, so dark mode never flashes light. Theme toggle in the topbar flips the attribute and persists it.
The live theme generator at `production/theme.html` rewrites the same custom properties in real time and lets users copy/download the SCSS overrides.
### Subpath deploys
`base` in [vite.config.js](vite.config.js) reads `process.env.BASE_PATH` for build/preview. Asset URLs (manifest, apple-touch-icon, service worker registration) all use `import.meta.env.BASE_URL` so deploys under e.g. `/theme/gentelella/` resolve correctly. The R2 deploy script (`npm run deploy:preview`) reads `PREVIEW_SLUG` and sets `BASE_PATH=/theme/$SLUG/` before building.
### Service worker
Registered only in `import.meta.env.PROD` (skips dev so HMR isn't fighting cache). Path: `${BASE_URL}sw.js` so it scopes correctly under a subpath. Deploy script uploads `sw.js` and `site.webmanifest` with `Cache-Control: no-cache` so users get the freshest service worker on every visit.
## Conventions
1. **Vanilla DOM only.** `querySelector`, `classList`, `addEventListener`. No jQuery shim, no SPA framework.
2. **Event delegation on `document`** for common interactions (toggles, todo checkboxes, chart tabs) — see the bottom half of [src/main-v4.js](src/main-v4.js). Components that own their own state (inbox, kanban, command palette) register listeners on their root element instead.
3. **Lazy import per-page modules** with a DOM-presence guard so the bundle never ships unused code:
```js
if (document.querySelector('.calendar-grid')) {
import('./v4/calendar.js').then((m) => m.initCalendar());
}
```
4. **Idempotent `init*()` functions.** Every module exports a single `init<Name>()` that is safe to call when its root element is absent and safe to call twice. The shell does this for you on every page; per-page modules do it themselves.
5. **`showModal()` and `showToast()`**, not hand-rolled overlays. Both in [src/v4/modal.js](src/v4/modal.js) / [src/v4/toast.js](src/v4/toast.js).
6. **`openMenu()` and `openPanel()`** ([src/v4/menus.js](src/v4/menus.js)) for any dropdown or slide-out — handles outside-click, escape, focus return.
7. **CSS custom properties for colors**, never hex literals in components. Defined in `_tokens.scss`, themed via `[data-theme="dark"]`. Charts read them via `getComputedStyle(document.documentElement).getPropertyValue('--…')`.
8. **ESLint single quotes + semicolons + 2-space indent.** Prettier formats. Both run pre-commit by convention; CI doesn't gate on them.
9. **No `console.log` in shipped code** — Terser drops `console.*` and `debugger` from production builds (see `terserOptions.compress` in [vite.config.js](vite.config.js)), but the lint config still flags them so you spot them in review.
## Anti-patterns
- Don't add jQuery, Bootstrap, or a SPA framework. v4's whole pitch is "vanilla and small."
- Don't hand-write Vite entry input lists — drop the file in `production/`.
- Don't bypass `mountShell()` to wire up your own sidebar/topbar. Use `data-shell="admin"` and let the plugin inject.
- Don't hard-code `/` paths in HTML or JS. Use relative paths inside `production/*.html` and `import.meta.env.BASE_URL` in JS.
- Don't import the whole of ECharts. The pattern in [src/v4/charts.js](src/v4/charts.js) does modular imports — match it.
- Don't write directly to `dist/` — it's the build output, gitignored, blown away on every build.
- Don't directly `new bootstrap.Modal(…)` — there is no Bootstrap. Use `showModal()`.
- Don't bump CDN-loaded scripts in templates without checking SRI hashes if any are pinned. (Most assets are bundled; check [production/index.html](production/index.html) and friends for `integrity=`.)
- Don't `Notification.objects.create()`-style direct DOM construction for toasts — use `showToast()`.
- Don't edit files in `dist/`, `node_modules/`, or `docs/screenshots/` — they're all generated.
## Recipes
### New chart card
1. Markup: `<div class="card chart-card"><div class="chart" data-chart="<id>"></div></div>` inside your page.
2. Add a `case '<id>':` in `initCharts()` in [src/v4/charts.js](src/v4/charts.js) that builds the ECharts `option` and returns it.
3. The wrapper reads tokens via `getComputedStyle` so dark mode redraw is automatic.
### New page in NAV
1. `npm run new -- <slug> --nav-group "<Group>"` — done.
2. Or by hand: append to the right group in `NAV` in [src/v4/shell-render.js](src/v4/shell-render.js), with `{ key, href, text, icon }`. Match `key` to your page's `data-page`.
### New modal or toast
```js
import { showModal } from './v4/modal.js';
showModal({ title: 'Delete project?', body: 'This can\'t be undone.', actions: [
{ label: 'Cancel', variant: 'ghost' },
{ label: 'Delete', variant: 'danger', action: () => { /* … */ } }
]});
import { showToast } from './v4/toast.js';
showToast('Saved', { variant: 'success' });
```
### Wire up keyboard shortcuts
Single global handler in [src/v4/command-palette.js](src/v4/command-palette.js) handles ⌘K. For page-local shortcuts (e.g. inbox J/K/R/S/#), register on the page module's root element and check `e.target.matches(':is(input,textarea,[contenteditable])')` first.
## Build output
```text
dist/
├── assets/ # Hashed CSS + fonts
├── images/ # Hashed images
├── js/ # Hashed JS chunks
├── production/ # 58 entry HTMLs (paths resolved at build time)
├── site.webmanifest
├── sw.js
└── stats.html # Bundle analyzer (stripped by deploy script)
```
The deploy script does three passes: long-cache hashed assets, short-cache HTML, no-cache `sw.js` + `site.webmanifest`. See [scripts/deploy-preview.sh](scripts/deploy-preview.sh) for the reasoning — Cloudflare APO will otherwise pin stale HTML pointing at deleted hashed assets.
## TypeScript
No `.ts` files, but [types/gentelella.d.ts](types/gentelella.d.ts) declares the public JS surface for IntelliSense. `package.json` `"types"` field points to it; VS Code picks it up automatically.
# AGENTS.md
Cross-tool agent instructions for Gentelella v4. Read by Aider, Cline, Codex, Continue, and any tool following the [agents.md](https://agents.md) convention. Claude Code reads `CLAUDE.md`; Cursor reads `.cursor/rules/`; GitHub Copilot reads `.github/copilot-instructions.md`. Content is intentionally overlapping — each tool only sees its own file.
## What this is
Gentelella v4 (`4.1.1`) — free admin dashboard template by Colorlib. 58 server-rendered HTML pages in [production/](production/), built with **Vite 8** (Rolldown). **Vanilla ES2022**, no Bootstrap, no jQuery, no SPA framework. SCSS only. Heavyweight runtime deps are limited to **ECharts 6**, **DataTables.net 3**, and **Leaflet 1.9** — all lazy-imported per page.
Live preview: <https://preview.colorlib.com/theme/gentelella/>.
## Setup
```bash
npm install
npm run dev # Vite dev server on :9173 → opens /production/index.html
```
Build / preview / deploy:
```bash
npm run build # → dist/
npm run preview # serve built dist/ on :9174
npm run deploy:preview # build + sync to R2 with cache headers
```
## Architecture
- **Single entry** [src/main-v4.js](src/main-v4.js). Imports `scss/v4/main.scss`, mounts the shell, runs `initCharts/initTables/initCommandPalette/initPageActions`, then lazy-imports page-specific modules guarded by DOM presence (`if (document.getElementById('inbox-root')) import(...)`).
- **Shell injection at build time.** [vite.config.js](vite.config.js)'s `shellInjectionPlugin` inlines sidebar/topbar/footer into every page whose body has `data-shell="admin"`. No FOUC. Runtime [src/v4/shell.js](src/v4/shell.js) `mountShell()` is a fallback for opening raw HTML.
- **Auto-discovered entries.** `discoverEntries()` in [vite.config.js](vite.config.js) walks `production/*.html` and registers each as a Rollup input. No hand-maintained input list.
- **Three lazy vendor chunks**: `vendor-echarts` (chart pages), `vendor-tables` (table pages), `vendor-maps` (map page). Everything else ships in the main chunk.
- **NAV is one constant.** `NAV` in [src/v4/shell-render.js](src/v4/shell-render.js), 7 groups. Pages match into NAV by `data-page` ↔ leaf `key`.
- **Theming via CSS custom properties.** Tokens in [src/scss/v4/_tokens.scss](src/scss/v4/_tokens.scss) under `:root` and `[data-theme="dark"]`. Pre-paint inline script (in the Vite plugin) sets `data-theme` on `<html>` from `localStorage` before body renders.
- **PWA.** Service worker registered only in `import.meta.env.PROD`. `site.webmanifest` + meta tags injected into every page by the Vite plugin. Subpath-safe: paths use `import.meta.env.BASE_URL`.
## Directory layout
```text
src/
main-v4.js # Entry — mounts shell, lazy-loads modules
scss/v4/ # 10 partials, main.scss is the @use'd entry
v4/
shell.js # mountShell — runtime shell behavior
shell-render.js # Pure renderers + NAV + ICONS
menus.js # openMenu / openPanel
modal.js # showModal
toast.js # showToast
charts.js # ECharts wrapper + factories
tables.js # DataTables wrapper
command-palette.js # ⌘K
page-actions.js
inbox.js kanban.js calendar.js settings.js file-manager.js
form-controls.js # Date range, multi-select, rich text
details.js markup.js data-adapter.js
product-images.js product-mockups.js
production/ # 58 HTML entry pages (auto-discovered)
public/ # Copied verbatim to dist/
types/gentelella.d.ts # Type declarations for the public JS surface
scripts/
new-page.mjs # npm run new -- <slug>
screenshots.mjs # npm run screenshots
smoke.mjs # npm run smoke
deploy-preview.sh # npm run deploy:preview
examples/ # Standalone integrations (Express/SQLite, etc.)
```
## Conventions
1. **Vanilla DOM only.** `querySelector`, `classList`, `addEventListener`. No jQuery, no SPA framework.
2. **Lazy import per-page modules** with a DOM-presence guard so the main bundle never ships unused code.
3. **Idempotent `init<Name>()` exports.** Safe to call when the root element is absent; safe to call twice.
4. **Event delegation on `document`** for common interactions (toggles, todo checkboxes, chart tabs) — see the bottom of [src/main-v4.js](src/main-v4.js). Components that own their state (inbox, kanban, command palette) register on their own root.
5. **`showModal()` / `showToast()`** ([v4/modal.js](src/v4/modal.js), [v4/toast.js](src/v4/toast.js)) for overlays; **`openMenu()` / `openPanel()`** ([v4/menus.js](src/v4/menus.js)) for dropdowns and slide-outs. Both handle outside-click / escape / focus return.
6. **CSS custom properties for colors.** Never hex literals in components. Charts read them via `getComputedStyle(document.documentElement).getPropertyValue('--…')` so dark-mode redraw is automatic.
7. **Subpath-safe URLs.** Use `import.meta.env.BASE_URL` in JS and `${base}` in the Vite plugin. Inside `production/*.html`, use relative paths.
8. **No `console.*` in shipped code.** Terser drops them in production builds; lint flags them so you catch them earlier.
9. **ESLint + Prettier** (single quotes, semicolons, 2-space indent). Run before committing; CI doesn't gate.
10. **Shell opt-in.** Pages without `data-shell="admin"` don't get a sidebar/topbar (login, marketing, error pages).
## Anti-patterns
- Don't add jQuery, Bootstrap, or any SPA framework. The whole pitch of v4 is "vanilla and small."
- Don't write Vite entry input lists by hand — drop the file in `production/`.
- Don't hand-roll your own modal/toast/dropdown — use [v4/modal.js](src/v4/modal.js), [v4/toast.js](src/v4/toast.js), [v4/menus.js](src/v4/menus.js).
- Don't hard-code `/` in asset paths. Use `import.meta.env.BASE_URL`.
- Don't bypass `mountShell()` to wire up sidebar/topbar yourself — set `data-shell="admin"` and let the Vite plugin inject.
- Don't import all of ECharts. Use modular imports — match the pattern in [src/v4/charts.js](src/v4/charts.js).
- Don't edit files in `dist/`, `node_modules/`, or `docs/screenshots/` — generated.
- Don't introduce a build step besides Vite. No PostCSS pipeline, no Webpack alongside, no Tailwind.
- Don't use `new bootstrap.Modal(...)` — there is no Bootstrap.
## Recipes
### Add a new page
Preferred — scaffolder writes the HTML, body attributes, and (optionally) the NAV entry:
```bash
npm run new -- reports --title "Reports" --nav-group "Admin"
npm run new -- user-roles --title "User roles" \
--breadcrumb "Home > User management|user_management.html > Roles" \
--nav-group "Admin" --icon profile
```
By hand:
1. `production/<slug>.html` with `<body data-shell="admin" data-page="<slug>" data-breadcrumb="Home > …">` and a `<script type="module" src="/src/main-v4.js"></script>` in `<head>`.
2. Append to the right group in `NAV` in [src/v4/shell-render.js](src/v4/shell-render.js). `key` matches `data-page`.
3. New icon? Add to `ICONS` in the same file (inline SVG, `currentColor` stroke).
Breadcrumb segments link automatically when their text matches a NAV item (`Forms` → `form.html`; a parent group resolves to its first child). Point anywhere else with a pipe — `data-breadcrumb="Home > Projects|projects.html > Acme Redesign"`. The last segment is the current page and is never a link. A segment with no match and no explicit target renders as plain text, so drop grouping-only levels (`Apps`, `Layouts`) rather than shipping a dead crumb.
### Add a chart
1. `<div class="card chart-card"><div class="chart" data-chart="<id>"></div></div>` in the page.
2. Add a `case '<id>':` in `initCharts()` in [src/v4/charts.js](src/v4/charts.js) that builds and returns the ECharts `option`.
3. Read colors via `getComputedStyle(document.documentElement).getPropertyValue('--token-name')` — dark mode redraw is automatic.
### Add a modal or 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' });
```
### Add a page-local module
```js
// At the bottom of src/main-v4.js:
if (document.querySelector('.reports-root')) {
import('./v4/reports.js').then((m) => m.initReports());
}
```
Export a single `initReports()` from `src/v4/reports.js`. Guard re-entry; idempotent.
## Subpath / deploy
```bash
BASE_PATH=/theme/gentelella/ npm run build # build under a subpath
PREVIEW_SLUG=gentelella npm run deploy:preview # build + R2 sync, scoped to /theme/gentelella/
```
[scripts/deploy-preview.sh](scripts/deploy-preview.sh) does three passes: long-cache for hashed assets, short-cache for HTML, no-cache for `sw.js` and `site.webmanifest`. This works around Cloudflare APO pinning stale HTML at deleted hashed asset URLs.
## TypeScript
No `.ts` files, but [types/gentelella.d.ts](types/gentelella.d.ts) declares the public JS surface. `package.json` `"types"` points to it; VS Code / your editor picks it up automatically for IntelliSense across `src/v4/*.js`.
## Commands reference
```bash
npm run dev # Dev server on :9173 (PORT to override)
npm run build # Production build → dist/
npm run preview # Serve dist/ on :9174
npm run lint # ESLint
npm run lint:fix
npm run format # Prettier write
npm run format:check
npm run new -- <slug> # Scaffold a page
npm run screenshots # 22 pages × light+dark → docs/screenshots/
npm run smoke # Boot dev server, fetch every page, assert 200
npm run analyze # Build + open dist/stats.html
npm run deploy:preview # Build + R2 sync
```
# GitHub Copilot Instructions — Gentelella v4
Admin dashboard template (`4.1.1`) by Colorlib. 58 server-rendered HTML pages in `production/`, built with **Vite 8** (Rolldown). **Vanilla ES2022**, no Bootstrap, no jQuery, no SPA framework. SCSS only. Heavyweight deps — **ECharts 6**, **DataTables.net 3**, **Leaflet 1.9** — are lazy-imported per page. Full reference: `CLAUDE.md`.
## Hard rules
- **Vanilla DOM only.** `querySelector`, `classList`, `addEventListener`. No jQuery, no SPA framework. v4's pitch is "vanilla and small."
- **Single entry**: `src/main-v4.js`. Page-specific modules are lazy-imported inside it, guarded by DOM presence. Don't add `<script>` tags per page.
- **Pages auto-discover.** Drop `production/<slug>.html` and `discoverEntries()` in `vite.config.js` picks it up — never edit `rollupOptions.input`.
- **Shell opt-in**: `<body data-shell="admin" data-page="<key>" data-breadcrumb="Home > …">`. The Vite plugin inlines sidebar/topbar/footer at build/dev time (no FOUC).
- **Breadcrumbs link themselves.** A segment matching a `NAV` item's text becomes a link (`Forms` → `form.html`; a parent resolves to its first child). Override with a pipe: `data-breadcrumb="Home > Projects|projects.html > Acme Redesign"`. The last segment is the current page and never links; untargeted segments stay plain text.
- **NAV is one constant** — `NAV` in `src/v4/shell-render.js`, 7 groups. `key` matches `data-page`. New icons go in the `ICONS` object in the same file.
- **Overlays go through helpers**: `showModal()`/`showToast()`/`openMenu()`/`openPanel()` from `src/v4/{modal,toast,menus}.js`. Never hand-roll a backdrop, escape handler, or focus return.
- **CSS custom properties for colors.** Tokens in `src/scss/v4/_tokens.scss` under `:root` and `[data-theme="dark"]`. Charts read them via `getComputedStyle(document.documentElement).getPropertyValue('--…')` so dark-mode redraw is automatic.
- **Lazy ECharts.** Match the modular import pattern in `src/v4/charts.js`. Don't `import * as echarts`.
- **Subpath-safe URLs.** `import.meta.env.BASE_URL` in JS, `${base}` in the Vite plugin, relative paths in `production/*.html`. Never hard-code a leading `/`.
- **Idempotent `init<Name>()` exports.** Every module in `src/v4/` has one. Safe to call when its root element is absent, safe to call twice.
- **No `console.*` in shipped code.** Terser drops them in production; ESLint flags earlier.
- **Service worker only in prod** (`import.meta.env.PROD` guard) — keeps HMR working in dev.
## File layout
- `src/main-v4.js` — entry; mounts shell + lazy-loads page modules
- `src/scss/v4/` — 10 SCSS partials (`_tokens`, `_layout`, `_components`, `_widgets`, `_forms`, `_datatable`, `_pages`, `_apps`, `_auth`, `main`)
- `src/v4/shell.js` — `mountShell()` runtime (sidebar accordion, theme toggle, mobile drawer)
- `src/v4/shell-render.js` — `NAV` + `ICONS` + pure renderers (also imported by Vite plugin)
- `src/v4/charts.js` — `initCharts()` + ECharts factories
- `src/v4/tables.js` — `initTables()` + DataTables wrapper
- `src/v4/command-palette.js` — ⌘K
- `src/v4/{modal,toast,menus}.js` — overlay helpers
- `src/v4/{inbox,kanban,calendar,settings,file-manager}.js` — page modules (lazy-loaded)
- `src/v4/form-controls.js` — date range, multi-select, rich text
- `production/` — 58 HTML entry pages, auto-discovered
- `public/` — static assets copied verbatim to `dist/`
- `types/gentelella.d.ts` — TypeScript declarations for the public JS surface
- `scripts/new-page.mjs` — page scaffolder (`npm run new -- <slug>`)
- `scripts/deploy-preview.sh` — R2 deploy with per-file cache headers
## Anti-patterns
- Don't add jQuery, Bootstrap, or a SPA framework.
- Don't write Vite entry input lists by hand.
- Don't add `<script>` tags to `production/*.html` for new modules — lazy-import in `src/main-v4.js`.
- Don't bypass `mountShell()` to wire your own sidebar/topbar.
- Don't `new bootstrap.Modal(...)` — there is no Bootstrap.
- Don't hard-code `/` paths in JS/HTML.
- Don't import all of ECharts — match `src/v4/charts.js`.
- Don't use hex colors in components — use CSS custom properties.
- Don't introduce PostCSS, Tailwind, or any pipeline alongside Vite.
- Don't edit `dist/`, `node_modules/`, or `docs/screenshots/`.
## Commands
```bash
npm run dev # Vite dev server on :9173 (set PORT to override)
npm run build # Production build → dist/
npm run preview # Serve dist/ on :9174
npm run lint # ESLint over src/
npm run lint:fix
npm run format # Prettier write
npm run new -- <slug> # Scaffold a page (use --nav-group, --icon, --title …)
npm run screenshots # Playwright capture (22 pages × light+dark)
npm run smoke # Boot dev server, hit every page, assert 200
npm run analyze # Build + open dist/stats.html
npm run deploy:preview # Build + R2 sync with cache headers
```
Build under a subpath: `BASE_PATH=/foo/ npm run build`.
## When generating code
- **New page** → use `npm run new -- <slug> --nav-group "<Group>"` rather than crafting the HTML by hand. If you do write by hand, copy the head/body pattern from `production/index.html` (specifically the `data-shell`/`data-page`/`data-breadcrumb` triplet and the `<script type="module" src="/src/main-v4.js">` tag).
- **New chart** → add a `case` to the switch inside `initCharts()` in `src/v4/charts.js`. Read tokens via `getComputedStyle(document.documentElement).getPropertyValue('--…')`.
- **New page module** → add the lazy-import block at the bottom of `src/main-v4.js`, then create `src/v4/<name>.js` exporting a single idempotent `init<Name>()`.
- **New NAV entry** → append to the right group in `NAV` in `src/v4/shell-render.js`. `{ key, href, text, icon }`. `key` must match the target page's `data-page`.
- **New icon** → append to `ICONS` in `src/v4/shell-render.js`. Inline SVG, `currentColor` stroke, 24×24 viewbox.
- **New modal/toast** → import `showModal` / `showToast` from `src/v4/modal.js` / `src/v4/toast.js`. Don't render backdrops yourself.
- **New SCSS** → add a partial under `src/scss/v4/` and `@use` it from `main.scss`. Variables live in `_tokens.scss`; new tokens get both a light and a dark value.