# 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.0.0`) — 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 2, and Leaflet 1.9 are the only heavyweight runtime deps — all lazy-imported per page. Live preview: . ## 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 -- # 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 ``. 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 > Admin > Roles" --nav-group "Admin" --icon profile ``` If you write the file by hand instead, the contract is: 1. Drop `production/.html`. Vite auto-discovers it (no config edit). 2. Set ``. 3. Add a `` in ``. 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). ### 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 `` 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()` 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: `
` inside your page. 2. Add a `case '':` 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 -- --nav-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.