{"owner":"ColorlibHQ","repo":"gentelella","hasSkills":true,"hasMcp":false,"mcpConfig":null,"found":["AGENTS.md","CLAUDE.md"],"skills":{"AGENTS.md":"# AGENTS.md\n\nCross-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.\n\n## What this is\n\nGentelella 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.\n\nLive preview: <https://preview.colorlib.com/theme/gentelella/>.\n\n## Setup\n\n```bash\nnpm install\nnpm run dev               # Vite dev server on :9173 → opens /production/index.html\n```\n\nBuild / preview / deploy:\n\n```bash\nnpm run build             # → dist/\nnpm run preview           # serve built dist/ on :9174\nnpm run deploy:preview    # build + sync to R2 with cache headers\n```\n\n## Architecture\n\n- **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(...)`).\n- **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.\n- **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.\n- **Three lazy vendor chunks**: `vendor-echarts` (chart pages), `vendor-tables` (table pages), `vendor-maps` (map page). Everything else ships in the main chunk.\n- **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`.\n- **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.\n- **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`.\n\n## Directory layout\n\n```text\nsrc/\n  main-v4.js               # Entry — mounts shell, lazy-loads modules\n  scss/v4/                 # 10 partials, main.scss is the @use'd entry\n  v4/\n    shell.js               # mountShell — runtime shell behavior\n    shell-render.js        # Pure renderers + NAV + ICONS\n    menus.js               # openMenu / openPanel\n    modal.js               # showModal\n    toast.js               # showToast\n    charts.js              # ECharts wrapper + factories\n    tables.js              # DataTables wrapper\n    command-palette.js     # ⌘K\n    page-actions.js\n    inbox.js kanban.js calendar.js settings.js file-manager.js\n    form-controls.js       # Date range, multi-select, rich text\n    details.js markup.js data-adapter.js\n    product-images.js product-mockups.js\nproduction/                # 58 HTML entry pages (auto-discovered)\npublic/                    # Copied verbatim to dist/\ntypes/gentelella.d.ts      # Type declarations for the public JS surface\nscripts/\n  new-page.mjs             # npm run new -- <slug>\n  screenshots.mjs          # npm run screenshots\n  smoke.mjs                # npm run smoke\n  deploy-preview.sh        # npm run deploy:preview\nexamples/                  # Standalone integrations (Express/SQLite, etc.)\n```\n\n## Conventions\n\n1. **Vanilla DOM only.** `querySelector`, `classList`, `addEventListener`. No jQuery, no SPA framework.\n2. **Lazy import per-page modules** with a DOM-presence guard so the main bundle never ships unused code.\n3. **Idempotent `init<Name>()` exports.** Safe to call when the root element is absent; safe to call twice.\n4. **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.\n5. **`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.\n6. **CSS custom properties for colors.** Never hex literals in components. Charts read them via `getComputedStyle(document.documentElement).getPropertyValue('--…')` so dark-mode redraw is automatic.\n7. **Subpath-safe URLs.** Use `import.meta.env.BASE_URL` in JS and `${base}` in the Vite plugin. Inside `production/*.html`, use relative paths.\n8. **No `console.*` in shipped code.** Terser drops them in production builds; lint flags them so you catch them earlier.\n9. **ESLint + Prettier** (single quotes, semicolons, 2-space indent). Run before committing; CI doesn't gate.\n10. **Shell opt-in.** Pages without `data-shell=\"admin\"` don't get a sidebar/topbar (login, marketing, error pages).\n\n## Anti-patterns\n\n- Don't add jQuery, Bootstrap, or any SPA framework. The whole pitch of v4 is \"vanilla and small.\"\n- Don't write Vite entry input lists by hand — drop the file in `production/`.\n- 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).\n- Don't hard-code `/` in asset paths. Use `import.meta.env.BASE_URL`.\n- Don't bypass `mountShell()` to wire up sidebar/topbar yourself — set `data-shell=\"admin\"` and let the Vite plugin inject.\n- Don't import all of ECharts. Use modular imports — match the pattern in [src/v4/charts.js](src/v4/charts.js).\n- Don't edit files in `dist/`, `node_modules/`, or `docs/screenshots/` — generated.\n- Don't introduce a build step besides Vite. No PostCSS pipeline, no Webpack alongside, no Tailwind.\n- Don't use `new bootstrap.Modal(...)` — there is no Bootstrap.\n\n## Recipes\n\n### Add a new page\n\nPreferred — scaffolder writes the HTML, body attributes, and (optionally) the NAV entry:\n\n```bash\nnpm run new -- reports --title \"Reports\" --nav-group \"Admin\"\nnpm run new -- user-roles --title \"User roles\" \\\n  --breadcrumb \"Home > User management|user_management.html > Roles\" \\\n  --nav-group \"Admin\" --icon profile\n```\n\nBy hand:\n\n1. `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>`.\n2. Append to the right group in `NAV` in [src/v4/shell-render.js](src/v4/shell-render.js). `key` matches `data-page`.\n3. New icon? Add to `ICONS` in the same file (inline SVG, `currentColor` stroke).\n\nBreadcrumb 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.\n\n### Add a chart\n\n1. `<div class=\"card chart-card\"><div class=\"chart\" data-chart=\"<id>\"></div></div>` in the page.\n2. Add a `case '<id>':` in `initCharts()` in [src/v4/charts.js](src/v4/charts.js) that builds and returns the ECharts `option`.\n3. Read colors via `getComputedStyle(document.documentElement).getPropertyValue('--token-name')` — dark mode redraw is automatic.\n\n### Add a modal or toast\n\n```js\nimport { showModal } from './v4/modal.js';\nshowModal({\n  title: 'Delete project?',\n  body: 'This cannot be undone.',\n  actions: [\n    { label: 'Cancel', variant: 'ghost' },\n    { label: 'Delete', variant: 'danger', action: () => { /* … */ } }\n  ]\n});\n\nimport { showToast } from './v4/toast.js';\nshowToast('Saved', { variant: 'success' });\n```\n\n### Add a page-local module\n\n```js\n// At the bottom of src/main-v4.js:\nif (document.querySelector('.reports-root')) {\n  import('./v4/reports.js').then((m) => m.initReports());\n}\n```\n\nExport a single `initReports()` from `src/v4/reports.js`. Guard re-entry; idempotent.\n\n## Subpath / deploy\n\n```bash\nBASE_PATH=/theme/gentelella/ npm run build      # build under a subpath\nPREVIEW_SLUG=gentelella npm run deploy:preview  # build + R2 sync, scoped to /theme/gentelella/\n```\n\n[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.\n\n## TypeScript\n\nNo `.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`.\n\n## Commands reference\n\n```bash\nnpm run dev                # Dev server on :9173 (PORT to override)\nnpm run build              # Production build → dist/\nnpm run preview            # Serve dist/ on :9174\nnpm run lint               # ESLint\nnpm run lint:fix\nnpm run format             # Prettier write\nnpm run format:check\nnpm run new -- <slug>      # Scaffold a page\nnpm run screenshots        # 22 pages × light+dark → docs/screenshots/\nnpm run smoke              # Boot dev server, fetch every page, assert 200\nnpm run analyze            # Build + open dist/stats.html\nnpm run deploy:preview     # Build + R2 sync\n```\n","CLAUDE.md":"# CLAUDE.md\n\nGuidance 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.\n\n## What this is\n\nGentelella 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.\n\nLive preview: <https://preview.colorlib.com/theme/gentelella/>.\n\n## Commands\n\n```bash\nnpm run dev                # Vite dev server on :9173, opens /production/index.html\nnpm run build              # Production build → dist/\nnpm run preview            # Serve built dist/ on :9174\n\nnpm run lint               # ESLint over src/\nnpm run lint:fix           # Auto-fix\nnpm run format             # Prettier write\nnpm run format:check       # Prettier check\n\nnpm run new -- <slug>      # Scaffold a new page under production/\nnpm run screenshots        # Playwright captures 22 pages × light+dark → docs/screenshots/\nnpm run smoke              # Boot dev server, hit every page, assert 200\nnpm run analyze            # Build + open dist/stats.html\nnpm run deploy:preview     # Build + sync to R2 with per-file cache headers\n```\n\nOverride the dev port via `PORT=…`; build under a subpath via `BASE_PATH=/foo/ npm run build`.\n\n## Architecture\n\n**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:\n\n```js\nif (document.getElementById('inbox-root')) {\n  import('./v4/inbox.js').then((m) => m.initInbox());\n}\n```\n\n**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).\n\n**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.\n\n**Chunking**: only three vendor chunks are emitted, all lazy:\n\n| Chunk            | Loaded on   | Source                          |\n| ---------------- | ----------- | ------------------------------- |\n| `vendor-echarts` | chart pages | `node_modules/echarts/`         |\n| `vendor-tables`  | table pages | `node_modules/datatables.net/`  |\n| `vendor-maps`    | map page    | `node_modules/leaflet/`         |\n\nEverything else (shell, command palette, charts wrapper, tables wrapper, etc.) is in the main chunk and is small enough not to need splitting.\n\n### Directory layout\n\n```text\nsrc/\n├── main-v4.js              # Entry — mounts shell, lazy-loads modules\n├── scss/\n│   ├── v4/\n│   │   ├── main.scss       # Entry — @use's the partials below\n│   │   ├── _tokens.scss    # CSS custom properties (light + dark)\n│   │   ├── _layout.scss    # Page wrapper, sidebar, topbar, grid\n│   │   ├── _components.scss# Buttons, cards, badges, forms, …\n│   │   ├── _widgets.scss   # Stat cards, mini-charts, todo lists, …\n│   │   ├── _forms.scss     # Inputs, switches, date pickers\n│   │   ├── _datatable.scss # DataTables re-skin\n│   │   ├── _pages.scss     # Per-page styles (kept narrow)\n│   │   ├── _apps.scss      # Inbox, kanban, chat, calendar, settings\n│   │   └── _auth.scss      # Login/register/forgot/2FA/lock/errors\n└── v4/\n    ├── shell.js            # mountShell — sidebar/topbar wiring\n    ├── shell-render.js     # Pure renderers + NAV definition (used by Vite plugin)\n    ├── menus.js            # openMenu/openPanel dropdowns\n    ├── modal.js            # showModal\n    ├── toast.js            # showToast\n    ├── charts.js           # ECharts factory + initCharts()\n    ├── tables.js           # DataTables initialiser\n    ├── command-palette.js  # ⌘K\n    ├── page-actions.js     # Per-page action button delegation\n    ├── inbox.js            # Folders, reader, compose\n    ├── kanban.js           # Drag/drop board\n    ├── calendar.js         # FullCalendar-style CRUD\n    ├── settings.js         # localStorage-backed settings page\n    ├── form-controls.js    # Date range, multi-select, rich text\n    ├── file-manager.js     # Tree + grid file browser\n    ├── details.js          # Disclosure rows\n    ├── markup.js           # HTML pretty-printer for component playground\n    ├── data-adapter.js     # Demo data shim\n    ├── product-images.js   # E-commerce gallery\n    └── product-mockups.js  # Storefront demo\n\nproduction/                 # 58 HTML entry pages (auto-discovered)\npublic/                     # Static assets copied verbatim to dist/\ntypes/gentelella.d.ts       # TypeScript declarations for the public JS surface\nscripts/\n├── new-page.mjs            # Scaffold a page + register in NAV\n├── screenshots.mjs         # Playwright capture (22 pages × 2 themes)\n├── smoke.mjs               # Boot dev server, fetch every page\n└── deploy-preview.sh       # Build + R2 sync + cache-header pass\n\nexamples/                   # Standalone integration examples (Express/SQLite, etc.)\n```\n\n### Adding a new page\n\nUse the scaffolder — it writes the HTML, sets the body attributes correctly, and (optionally) inserts the page into NAV:\n\n```bash\nnpm run new -- reports --title \"Reports\" --nav-group \"Admin\"\nnpm run new -- user-roles --title \"User roles\" \\\n  --breadcrumb \"Home > User management|user_management.html > Roles\" \\\n  --nav-group \"Admin\" --icon profile\n```\n\nIf you write the file by hand instead, the contract is:\n\n1. Drop `production/<slug>.html`. Vite auto-discovers it (no config edit).\n2. Set `<body data-shell=\"admin\" data-page=\"<slug>\" data-breadcrumb=\"Home > …\">`.\n3. Add a `<script type=\"module\" src=\"/src/main-v4.js\"></script>` in `<head>`.\n4. To appear in the sidebar, edit `NAV` in [src/v4/shell-render.js](src/v4/shell-render.js) — match key to your `data-page`.\n\n### NAV and icons\n\nSingle 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`.\n\nIcons 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).\n\n### Breadcrumbs\n\n`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:\n\n1. **Explicit target** — `data-breadcrumb=\"Home > Projects|projects.html > Acme Redesign\"`. Everything after `|` is the href.\n2. **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.\n3. **Neither** — plain text, no link.\n\nLinks 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.\n\nPrefer 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.\n\n### Theming\n\nTokens 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.\n\nThe live theme generator at `production/theme.html` rewrites the same custom properties in real time and lets users copy/download the SCSS overrides.\n\n### Subpath deploys\n\n`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.\n\n### Service worker\n\nRegistered 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.\n\n## Conventions\n\n1. **Vanilla DOM only.** `querySelector`, `classList`, `addEventListener`. No jQuery shim, no SPA framework.\n2. **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.\n3. **Lazy import per-page modules** with a DOM-presence guard so the bundle never ships unused code:\n\n   ```js\n   if (document.querySelector('.calendar-grid')) {\n     import('./v4/calendar.js').then((m) => m.initCalendar());\n   }\n   ```\n\n4. **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.\n5. **`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).\n6. **`openMenu()` and `openPanel()`** ([src/v4/menus.js](src/v4/menus.js)) for any dropdown or slide-out — handles outside-click, escape, focus return.\n7. **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('--…')`.\n8. **ESLint single quotes + semicolons + 2-space indent.** Prettier formats. Both run pre-commit by convention; CI doesn't gate on them.\n9. **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.\n\n## Anti-patterns\n\n- Don't add jQuery, Bootstrap, or a SPA framework. v4's whole pitch is \"vanilla and small.\"\n- Don't hand-write Vite entry input lists — drop the file in `production/`.\n- Don't bypass `mountShell()` to wire up your own sidebar/topbar. Use `data-shell=\"admin\"` and let the plugin inject.\n- Don't hard-code `/` paths in HTML or JS. Use relative paths inside `production/*.html` and `import.meta.env.BASE_URL` in JS.\n- Don't import the whole of ECharts. The pattern in [src/v4/charts.js](src/v4/charts.js) does modular imports — match it.\n- Don't write directly to `dist/` — it's the build output, gitignored, blown away on every build.\n- Don't directly `new bootstrap.Modal(…)` — there is no Bootstrap. Use `showModal()`.\n- 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=`.)\n- Don't `Notification.objects.create()`-style direct DOM construction for toasts — use `showToast()`.\n- Don't edit files in `dist/`, `node_modules/`, or `docs/screenshots/` — they're all generated.\n\n## Recipes\n\n### New chart card\n\n1. Markup: `<div class=\"card chart-card\"><div class=\"chart\" data-chart=\"<id>\"></div></div>` inside your page.\n2. Add a `case '<id>':` in `initCharts()` in [src/v4/charts.js](src/v4/charts.js) that builds the ECharts `option` and returns it.\n3. The wrapper reads tokens via `getComputedStyle` so dark mode redraw is automatic.\n\n### New page in NAV\n\n1. `npm run new -- <slug> --nav-group \"<Group>\"` — done.\n2. 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`.\n\n### New modal or toast\n\n```js\nimport { showModal } from './v4/modal.js';\nshowModal({ title: 'Delete project?', body: 'This can\\'t be undone.', actions: [\n  { label: 'Cancel', variant: 'ghost' },\n  { label: 'Delete', variant: 'danger', action: () => { /* … */ } }\n]});\n\nimport { showToast } from './v4/toast.js';\nshowToast('Saved', { variant: 'success' });\n```\n\n### Wire up keyboard shortcuts\n\nSingle 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.\n\n## Build output\n\n```text\ndist/\n├── assets/         # Hashed CSS + fonts\n├── images/         # Hashed images\n├── js/             # Hashed JS chunks\n├── production/     # 58 entry HTMLs (paths resolved at build time)\n├── site.webmanifest\n├── sw.js\n└── stats.html      # Bundle analyzer (stripped by deploy script)\n```\n\nThe 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.\n\n## TypeScript\n\nNo `.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.\n"},"files":{"AGENTS.md":"# AGENTS.md\n\nCross-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.\n\n## What this is\n\nGentelella 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.\n\nLive preview: <https://preview.colorlib.com/theme/gentelella/>.\n\n## Setup\n\n```bash\nnpm install\nnpm run dev               # Vite dev server on :9173 → opens /production/index.html\n```\n\nBuild / preview / deploy:\n\n```bash\nnpm run build             # → dist/\nnpm run preview           # serve built dist/ on :9174\nnpm run deploy:preview    # build + sync to R2 with cache headers\n```\n\n## Architecture\n\n- **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(...)`).\n- **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.\n- **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.\n- **Three lazy vendor chunks**: `vendor-echarts` (chart pages), `vendor-tables` (table pages), `vendor-maps` (map page). Everything else ships in the main chunk.\n- **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`.\n- **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.\n- **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`.\n\n## Directory layout\n\n```text\nsrc/\n  main-v4.js               # Entry — mounts shell, lazy-loads modules\n  scss/v4/                 # 10 partials, main.scss is the @use'd entry\n  v4/\n    shell.js               # mountShell — runtime shell behavior\n    shell-render.js        # Pure renderers + NAV + ICONS\n    menus.js               # openMenu / openPanel\n    modal.js               # showModal\n    toast.js               # showToast\n    charts.js              # ECharts wrapper + factories\n    tables.js              # DataTables wrapper\n    command-palette.js     # ⌘K\n    page-actions.js\n    inbox.js kanban.js calendar.js settings.js file-manager.js\n    form-controls.js       # Date range, multi-select, rich text\n    details.js markup.js data-adapter.js\n    product-images.js product-mockups.js\nproduction/                # 58 HTML entry pages (auto-discovered)\npublic/                    # Copied verbatim to dist/\ntypes/gentelella.d.ts      # Type declarations for the public JS surface\nscripts/\n  new-page.mjs             # npm run new -- <slug>\n  screenshots.mjs          # npm run screenshots\n  smoke.mjs                # npm run smoke\n  deploy-preview.sh        # npm run deploy:preview\nexamples/                  # Standalone integrations (Express/SQLite, etc.)\n```\n\n## Conventions\n\n1. **Vanilla DOM only.** `querySelector`, `classList`, `addEventListener`. No jQuery, no SPA framework.\n2. **Lazy import per-page modules** with a DOM-presence guard so the main bundle never ships unused code.\n3. **Idempotent `init<Name>()` exports.** Safe to call when the root element is absent; safe to call twice.\n4. **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.\n5. **`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.\n6. **CSS custom properties for colors.** Never hex literals in components. Charts read them via `getComputedStyle(document.documentElement).getPropertyValue('--…')` so dark-mode redraw is automatic.\n7. **Subpath-safe URLs.** Use `import.meta.env.BASE_URL` in JS and `${base}` in the Vite plugin. Inside `production/*.html`, use relative paths.\n8. **No `console.*` in shipped code.** Terser drops them in production builds; lint flags them so you catch them earlier.\n9. **ESLint + Prettier** (single quotes, semicolons, 2-space indent). Run before committing; CI doesn't gate.\n10. **Shell opt-in.** Pages without `data-shell=\"admin\"` don't get a sidebar/topbar (login, marketing, error pages).\n\n## Anti-patterns\n\n- Don't add jQuery, Bootstrap, or any SPA framework. The whole pitch of v4 is \"vanilla and small.\"\n- Don't write Vite entry input lists by hand — drop the file in `production/`.\n- 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).\n- Don't hard-code `/` in asset paths. Use `import.meta.env.BASE_URL`.\n- Don't bypass `mountShell()` to wire up sidebar/topbar yourself — set `data-shell=\"admin\"` and let the Vite plugin inject.\n- Don't import all of ECharts. Use modular imports — match the pattern in [src/v4/charts.js](src/v4/charts.js).\n- Don't edit files in `dist/`, `node_modules/`, or `docs/screenshots/` — generated.\n- Don't introduce a build step besides Vite. No PostCSS pipeline, no Webpack alongside, no Tailwind.\n- Don't use `new bootstrap.Modal(...)` — there is no Bootstrap.\n\n## Recipes\n\n### Add a new page\n\nPreferred — scaffolder writes the HTML, body attributes, and (optionally) the NAV entry:\n\n```bash\nnpm run new -- reports --title \"Reports\" --nav-group \"Admin\"\nnpm run new -- user-roles --title \"User roles\" \\\n  --breadcrumb \"Home > User management|user_management.html > Roles\" \\\n  --nav-group \"Admin\" --icon profile\n```\n\nBy hand:\n\n1. `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>`.\n2. Append to the right group in `NAV` in [src/v4/shell-render.js](src/v4/shell-render.js). `key` matches `data-page`.\n3. New icon? Add to `ICONS` in the same file (inline SVG, `currentColor` stroke).\n\nBreadcrumb 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.\n\n### Add a chart\n\n1. `<div class=\"card chart-card\"><div class=\"chart\" data-chart=\"<id>\"></div></div>` in the page.\n2. Add a `case '<id>':` in `initCharts()` in [src/v4/charts.js](src/v4/charts.js) that builds and returns the ECharts `option`.\n3. Read colors via `getComputedStyle(document.documentElement).getPropertyValue('--token-name')` — dark mode redraw is automatic.\n\n### Add a modal or toast\n\n```js\nimport { showModal } from './v4/modal.js';\nshowModal({\n  title: 'Delete project?',\n  body: 'This cannot be undone.',\n  actions: [\n    { label: 'Cancel', variant: 'ghost' },\n    { label: 'Delete', variant: 'danger', action: () => { /* … */ } }\n  ]\n});\n\nimport { showToast } from './v4/toast.js';\nshowToast('Saved', { variant: 'success' });\n```\n\n### Add a page-local module\n\n```js\n// At the bottom of src/main-v4.js:\nif (document.querySelector('.reports-root')) {\n  import('./v4/reports.js').then((m) => m.initReports());\n}\n```\n\nExport a single `initReports()` from `src/v4/reports.js`. Guard re-entry; idempotent.\n\n## Subpath / deploy\n\n```bash\nBASE_PATH=/theme/gentelella/ npm run build      # build under a subpath\nPREVIEW_SLUG=gentelella npm run deploy:preview  # build + R2 sync, scoped to /theme/gentelella/\n```\n\n[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.\n\n## TypeScript\n\nNo `.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`.\n\n## Commands reference\n\n```bash\nnpm run dev                # Dev server on :9173 (PORT to override)\nnpm run build              # Production build → dist/\nnpm run preview            # Serve dist/ on :9174\nnpm run lint               # ESLint\nnpm run lint:fix\nnpm run format             # Prettier write\nnpm run format:check\nnpm run new -- <slug>      # Scaffold a page\nnpm run screenshots        # 22 pages × light+dark → docs/screenshots/\nnpm run smoke              # Boot dev server, fetch every page, assert 200\nnpm run analyze            # Build + open dist/stats.html\nnpm run deploy:preview     # Build + R2 sync\n```\n","CLAUDE.md":"# CLAUDE.md\n\nGuidance 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.\n\n## What this is\n\nGentelella 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.\n\nLive preview: <https://preview.colorlib.com/theme/gentelella/>.\n\n## Commands\n\n```bash\nnpm run dev                # Vite dev server on :9173, opens /production/index.html\nnpm run build              # Production build → dist/\nnpm run preview            # Serve built dist/ on :9174\n\nnpm run lint               # ESLint over src/\nnpm run lint:fix           # Auto-fix\nnpm run format             # Prettier write\nnpm run format:check       # Prettier check\n\nnpm run new -- <slug>      # Scaffold a new page under production/\nnpm run screenshots        # Playwright captures 22 pages × light+dark → docs/screenshots/\nnpm run smoke              # Boot dev server, hit every page, assert 200\nnpm run analyze            # Build + open dist/stats.html\nnpm run deploy:preview     # Build + sync to R2 with per-file cache headers\n```\n\nOverride the dev port via `PORT=…`; build under a subpath via `BASE_PATH=/foo/ npm run build`.\n\n## Architecture\n\n**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:\n\n```js\nif (document.getElementById('inbox-root')) {\n  import('./v4/inbox.js').then((m) => m.initInbox());\n}\n```\n\n**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).\n\n**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.\n\n**Chunking**: only three vendor chunks are emitted, all lazy:\n\n| Chunk            | Loaded on   | Source                          |\n| ---------------- | ----------- | ------------------------------- |\n| `vendor-echarts` | chart pages | `node_modules/echarts/`         |\n| `vendor-tables`  | table pages | `node_modules/datatables.net/`  |\n| `vendor-maps`    | map page    | `node_modules/leaflet/`         |\n\nEverything else (shell, command palette, charts wrapper, tables wrapper, etc.) is in the main chunk and is small enough not to need splitting.\n\n### Directory layout\n\n```text\nsrc/\n├── main-v4.js              # Entry — mounts shell, lazy-loads modules\n├── scss/\n│   ├── v4/\n│   │   ├── main.scss       # Entry — @use's the partials below\n│   │   ├── _tokens.scss    # CSS custom properties (light + dark)\n│   │   ├── _layout.scss    # Page wrapper, sidebar, topbar, grid\n│   │   ├── _components.scss# Buttons, cards, badges, forms, …\n│   │   ├── _widgets.scss   # Stat cards, mini-charts, todo lists, …\n│   │   ├── _forms.scss     # Inputs, switches, date pickers\n│   │   ├── _datatable.scss # DataTables re-skin\n│   │   ├── _pages.scss     # Per-page styles (kept narrow)\n│   │   ├── _apps.scss      # Inbox, kanban, chat, calendar, settings\n│   │   └── _auth.scss      # Login/register/forgot/2FA/lock/errors\n└── v4/\n    ├── shell.js            # mountShell — sidebar/topbar wiring\n    ├── shell-render.js     # Pure renderers + NAV definition (used by Vite plugin)\n    ├── menus.js            # openMenu/openPanel dropdowns\n    ├── modal.js            # showModal\n    ├── toast.js            # showToast\n    ├── charts.js           # ECharts factory + initCharts()\n    ├── tables.js           # DataTables initialiser\n    ├── command-palette.js  # ⌘K\n    ├── page-actions.js     # Per-page action button delegation\n    ├── inbox.js            # Folders, reader, compose\n    ├── kanban.js           # Drag/drop board\n    ├── calendar.js         # FullCalendar-style CRUD\n    ├── settings.js         # localStorage-backed settings page\n    ├── form-controls.js    # Date range, multi-select, rich text\n    ├── file-manager.js     # Tree + grid file browser\n    ├── details.js          # Disclosure rows\n    ├── markup.js           # HTML pretty-printer for component playground\n    ├── data-adapter.js     # Demo data shim\n    ├── product-images.js   # E-commerce gallery\n    └── product-mockups.js  # Storefront demo\n\nproduction/                 # 58 HTML entry pages (auto-discovered)\npublic/                     # Static assets copied verbatim to dist/\ntypes/gentelella.d.ts       # TypeScript declarations for the public JS surface\nscripts/\n├── new-page.mjs            # Scaffold a page + register in NAV\n├── screenshots.mjs         # Playwright capture (22 pages × 2 themes)\n├── smoke.mjs               # Boot dev server, fetch every page\n└── deploy-preview.sh       # Build + R2 sync + cache-header pass\n\nexamples/                   # Standalone integration examples (Express/SQLite, etc.)\n```\n\n### Adding a new page\n\nUse the scaffolder — it writes the HTML, sets the body attributes correctly, and (optionally) inserts the page into NAV:\n\n```bash\nnpm run new -- reports --title \"Reports\" --nav-group \"Admin\"\nnpm run new -- user-roles --title \"User roles\" \\\n  --breadcrumb \"Home > User management|user_management.html > Roles\" \\\n  --nav-group \"Admin\" --icon profile\n```\n\nIf you write the file by hand instead, the contract is:\n\n1. Drop `production/<slug>.html`. Vite auto-discovers it (no config edit).\n2. Set `<body data-shell=\"admin\" data-page=\"<slug>\" data-breadcrumb=\"Home > …\">`.\n3. Add a `<script type=\"module\" src=\"/src/main-v4.js\"></script>` in `<head>`.\n4. To appear in the sidebar, edit `NAV` in [src/v4/shell-render.js](src/v4/shell-render.js) — match key to your `data-page`.\n\n### NAV and icons\n\nSingle 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`.\n\nIcons 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).\n\n### Breadcrumbs\n\n`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:\n\n1. **Explicit target** — `data-breadcrumb=\"Home > Projects|projects.html > Acme Redesign\"`. Everything after `|` is the href.\n2. **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.\n3. **Neither** — plain text, no link.\n\nLinks 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.\n\nPrefer 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.\n\n### Theming\n\nTokens 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.\n\nThe live theme generator at `production/theme.html` rewrites the same custom properties in real time and lets users copy/download the SCSS overrides.\n\n### Subpath deploys\n\n`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.\n\n### Service worker\n\nRegistered 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.\n\n## Conventions\n\n1. **Vanilla DOM only.** `querySelector`, `classList`, `addEventListener`. No jQuery shim, no SPA framework.\n2. **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.\n3. **Lazy import per-page modules** with a DOM-presence guard so the bundle never ships unused code:\n\n   ```js\n   if (document.querySelector('.calendar-grid')) {\n     import('./v4/calendar.js').then((m) => m.initCalendar());\n   }\n   ```\n\n4. **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.\n5. **`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).\n6. **`openMenu()` and `openPanel()`** ([src/v4/menus.js](src/v4/menus.js)) for any dropdown or slide-out — handles outside-click, escape, focus return.\n7. **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('--…')`.\n8. **ESLint single quotes + semicolons + 2-space indent.** Prettier formats. Both run pre-commit by convention; CI doesn't gate on them.\n9. **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.\n\n## Anti-patterns\n\n- Don't add jQuery, Bootstrap, or a SPA framework. v4's whole pitch is \"vanilla and small.\"\n- Don't hand-write Vite entry input lists — drop the file in `production/`.\n- Don't bypass `mountShell()` to wire up your own sidebar/topbar. Use `data-shell=\"admin\"` and let the plugin inject.\n- Don't hard-code `/` paths in HTML or JS. Use relative paths inside `production/*.html` and `import.meta.env.BASE_URL` in JS.\n- Don't import the whole of ECharts. The pattern in [src/v4/charts.js](src/v4/charts.js) does modular imports — match it.\n- Don't write directly to `dist/` — it's the build output, gitignored, blown away on every build.\n- Don't directly `new bootstrap.Modal(…)` — there is no Bootstrap. Use `showModal()`.\n- 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=`.)\n- Don't `Notification.objects.create()`-style direct DOM construction for toasts — use `showToast()`.\n- Don't edit files in `dist/`, `node_modules/`, or `docs/screenshots/` — they're all generated.\n\n## Recipes\n\n### New chart card\n\n1. Markup: `<div class=\"card chart-card\"><div class=\"chart\" data-chart=\"<id>\"></div></div>` inside your page.\n2. Add a `case '<id>':` in `initCharts()` in [src/v4/charts.js](src/v4/charts.js) that builds the ECharts `option` and returns it.\n3. The wrapper reads tokens via `getComputedStyle` so dark mode redraw is automatic.\n\n### New page in NAV\n\n1. `npm run new -- <slug> --nav-group \"<Group>\"` — done.\n2. 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`.\n\n### New modal or toast\n\n```js\nimport { showModal } from './v4/modal.js';\nshowModal({ title: 'Delete project?', body: 'This can\\'t be undone.', actions: [\n  { label: 'Cancel', variant: 'ghost' },\n  { label: 'Delete', variant: 'danger', action: () => { /* … */ } }\n]});\n\nimport { showToast } from './v4/toast.js';\nshowToast('Saved', { variant: 'success' });\n```\n\n### Wire up keyboard shortcuts\n\nSingle 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.\n\n## Build output\n\n```text\ndist/\n├── assets/         # Hashed CSS + fonts\n├── images/         # Hashed images\n├── js/             # Hashed JS chunks\n├── production/     # 58 entry HTMLs (paths resolved at build time)\n├── site.webmanifest\n├── sw.js\n└── stats.html      # Bundle analyzer (stripped by deploy script)\n```\n\nThe 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.\n\n## TypeScript\n\nNo `.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.\n"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","title":"AGENTS.md","content":"# AGENTS.md\n\nCross-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.\n\n## What this is\n\nGentelella 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.\n\nLive preview: <https://preview.colorlib.com/theme/gentelella/>.\n\n## Setup\n\n```bash\nnpm install\nnpm run dev               # Vite dev server on :9173 → opens /production/index.html\n```\n\nBuild / preview / deploy:\n\n```bash\nnpm run build             # → dist/\nnpm run preview           # serve built dist/ on :9174\nnpm run deploy:preview    # build + sync to R2 with cache headers\n```\n\n## Architecture\n\n- **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(...)`).\n- **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.\n- **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.\n- **Three lazy vendor chunks**: `vendor-echarts` (chart pages), `vendor-tables` (table pages), `vendor-maps` (map page). Everything else ships in the main chunk.\n- **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`.\n- **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.\n- **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`.\n\n## Directory layout\n\n```text\nsrc/\n  main-v4.js               # Entry — mounts shell, lazy-loads modules\n  scss/v4/                 # 10 partials, main.scss is the @use'd entry\n  v4/\n    shell.js               # mountShell — runtime shell behavior\n    shell-render.js        # Pure renderers + NAV + ICONS\n    menus.js               # openMenu / openPanel\n    modal.js               # showModal\n    toast.js               # showToast\n    charts.js              # ECharts wrapper + factories\n    tables.js              # DataTables wrapper\n    command-palette.js     # ⌘K\n    page-actions.js\n    inbox.js kanban.js calendar.js settings.js file-manager.js\n    form-controls.js       # Date range, multi-select, rich text\n    details.js markup.js data-adapter.js\n    product-images.js product-mockups.js\nproduction/                # 58 HTML entry pages (auto-discovered)\npublic/                    # Copied verbatim to dist/\ntypes/gentelella.d.ts      # Type declarations for the public JS surface\nscripts/\n  new-page.mjs             # npm run new -- <slug>\n  screenshots.mjs          # npm run screenshots\n  smoke.mjs                # npm run smoke\n  deploy-preview.sh        # npm run deploy:preview\nexamples/                  # Standalone integrations (Express/SQLite, etc.)\n```\n\n## Conventions\n\n1. **Vanilla DOM only.** `querySelector`, `classList`, `addEventListener`. No jQuery, no SPA framework.\n2. **Lazy import per-page modules** with a DOM-presence guard so the main bundle never ships unused code.\n3. **Idempotent `init<Name>()` exports.** Safe to call when the root element is absent; safe to call twice.\n4. **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.\n5. **`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.\n6. **CSS custom properties for colors.** Never hex literals in components. Charts read them via `getComputedStyle(document.documentElement).getPropertyValue('--…')` so dark-mode redraw is automatic.\n7. **Subpath-safe URLs.** Use `import.meta.env.BASE_URL` in JS and `${base}` in the Vite plugin. Inside `production/*.html`, use relative paths.\n8. **No `console.*` in shipped code.** Terser drops them in production builds; lint flags them so you catch them earlier.\n9. **ESLint + Prettier** (single quotes, semicolons, 2-space indent). Run before committing; CI doesn't gate.\n10. **Shell opt-in.** Pages without `data-shell=\"admin\"` don't get a sidebar/topbar (login, marketing, error pages).\n\n## Anti-patterns\n\n- Don't add jQuery, Bootstrap, or any SPA framework. The whole pitch of v4 is \"vanilla and small.\"\n- Don't write Vite entry input lists by hand — drop the file in `production/`.\n- 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).\n- Don't hard-code `/` in asset paths. Use `import.meta.env.BASE_URL`.\n- Don't bypass `mountShell()` to wire up sidebar/topbar yourself — set `data-shell=\"admin\"` and let the Vite plugin inject.\n- Don't import all of ECharts. Use modular imports — match the pattern in [src/v4/charts.js](src/v4/charts.js).\n- Don't edit files in `dist/`, `node_modules/`, or `docs/screenshots/` — generated.\n- Don't introduce a build step besides Vite. No PostCSS pipeline, no Webpack alongside, no Tailwind.\n- Don't use `new bootstrap.Modal(...)` — there is no Bootstrap.\n\n## Recipes\n\n### Add a new page\n\nPreferred — scaffolder writes the HTML, body attributes, and (optionally) the NAV entry:\n\n```bash\nnpm run new -- reports --title \"Reports\" --nav-group \"Admin\"\nnpm run new -- user-roles --title \"User roles\" \\\n  --breadcrumb \"Home > User management|user_management.html > Roles\" \\\n  --nav-group \"Admin\" --icon profile\n```\n\nBy hand:\n\n1. `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>`.\n2. Append to the right group in `NAV` in [src/v4/shell-render.js](src/v4/shell-render.js). `key` matches `data-page`.\n3. New icon? Add to `ICONS` in the same file (inline SVG, `currentColor` stroke).\n\nBreadcrumb 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.\n\n### Add a chart\n\n1. `<div class=\"card chart-card\"><div class=\"chart\" data-chart=\"<id>\"></div></div>` in the page.\n2. Add a `case '<id>':` in `initCharts()` in [src/v4/charts.js](src/v4/charts.js) that builds and returns the ECharts `option`.\n3. Read colors via `getComputedStyle(document.documentElement).getPropertyValue('--token-name')` — dark mode redraw is automatic.\n\n### Add a modal or toast\n\n```js\nimport { showModal } from './v4/modal.js';\nshowModal({\n  title: 'Delete project?',\n  body: 'This cannot be undone.',\n  actions: [\n    { label: 'Cancel', variant: 'ghost' },\n    { label: 'Delete', variant: 'danger', action: () => { /* … */ } }\n  ]\n});\n\nimport { showToast } from './v4/toast.js';\nshowToast('Saved', { variant: 'success' });\n```\n\n### Add a page-local module\n\n```js\n// At the bottom of src/main-v4.js:\nif (document.querySelector('.reports-root')) {\n  import('./v4/reports.js').then((m) => m.initReports());\n}\n```\n\nExport a single `initReports()` from `src/v4/reports.js`. Guard re-entry; idempotent.\n\n## Subpath / deploy\n\n```bash\nBASE_PATH=/theme/gentelella/ npm run build      # build under a subpath\nPREVIEW_SLUG=gentelella npm run deploy:preview  # build + R2 sync, scoped to /theme/gentelella/\n```\n\n[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.\n\n## TypeScript\n\nNo `.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`.\n\n## Commands reference\n\n```bash\nnpm run dev                # Dev server on :9173 (PORT to override)\nnpm run build              # Production build → dist/\nnpm run preview            # Serve dist/ on :9174\nnpm run lint               # ESLint\nnpm run lint:fix\nnpm run format             # Prettier write\nnpm run format:check\nnpm run new -- <slug>      # Scaffold a page\nnpm run screenshots        # 22 pages × light+dark → docs/screenshots/\nnpm run smoke              # Boot dev server, fetch every page, assert 200\nnpm run analyze            # Build + open dist/stats.html\nnpm run deploy:preview     # Build + R2 sync\n```\n","category":"root","tokens":2481},{"name":"CLAUDE.md","path":"CLAUDE.md","title":"CLAUDE.md","content":"# CLAUDE.md\n\nGuidance 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.\n\n## What this is\n\nGentelella 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.\n\nLive preview: <https://preview.colorlib.com/theme/gentelella/>.\n\n## Commands\n\n```bash\nnpm run dev                # Vite dev server on :9173, opens /production/index.html\nnpm run build              # Production build → dist/\nnpm run preview            # Serve built dist/ on :9174\n\nnpm run lint               # ESLint over src/\nnpm run lint:fix           # Auto-fix\nnpm run format             # Prettier write\nnpm run format:check       # Prettier check\n\nnpm run new -- <slug>      # Scaffold a new page under production/\nnpm run screenshots        # Playwright captures 22 pages × light+dark → docs/screenshots/\nnpm run smoke              # Boot dev server, hit every page, assert 200\nnpm run analyze            # Build + open dist/stats.html\nnpm run deploy:preview     # Build + sync to R2 with per-file cache headers\n```\n\nOverride the dev port via `PORT=…`; build under a subpath via `BASE_PATH=/foo/ npm run build`.\n\n## Architecture\n\n**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:\n\n```js\nif (document.getElementById('inbox-root')) {\n  import('./v4/inbox.js').then((m) => m.initInbox());\n}\n```\n\n**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).\n\n**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.\n\n**Chunking**: only three vendor chunks are emitted, all lazy:\n\n| Chunk            | Loaded on   | Source                          |\n| ---------------- | ----------- | ------------------------------- |\n| `vendor-echarts` | chart pages | `node_modules/echarts/`         |\n| `vendor-tables`  | table pages | `node_modules/datatables.net/`  |\n| `vendor-maps`    | map page    | `node_modules/leaflet/`         |\n\nEverything else (shell, command palette, charts wrapper, tables wrapper, etc.) is in the main chunk and is small enough not to need splitting.\n\n### Directory layout\n\n```text\nsrc/\n├── main-v4.js              # Entry — mounts shell, lazy-loads modules\n├── scss/\n│   ├── v4/\n│   │   ├── main.scss       # Entry — @use's the partials below\n│   │   ├── _tokens.scss    # CSS custom properties (light + dark)\n│   │   ├── _layout.scss    # Page wrapper, sidebar, topbar, grid\n│   │   ├── _components.scss# Buttons, cards, badges, forms, …\n│   │   ├── _widgets.scss   # Stat cards, mini-charts, todo lists, …\n│   │   ├── _forms.scss     # Inputs, switches, date pickers\n│   │   ├── _datatable.scss # DataTables re-skin\n│   │   ├── _pages.scss     # Per-page styles (kept narrow)\n│   │   ├── _apps.scss      # Inbox, kanban, chat, calendar, settings\n│   │   └── _auth.scss      # Login/register/forgot/2FA/lock/errors\n└── v4/\n    ├── shell.js            # mountShell — sidebar/topbar wiring\n    ├── shell-render.js     # Pure renderers + NAV definition (used by Vite plugin)\n    ├── menus.js            # openMenu/openPanel dropdowns\n    ├── modal.js            # showModal\n    ├── toast.js            # showToast\n    ├── charts.js           # ECharts factory + initCharts()\n    ├── tables.js           # DataTables initialiser\n    ├── command-palette.js  # ⌘K\n    ├── page-actions.js     # Per-page action button delegation\n    ├── inbox.js            # Folders, reader, compose\n    ├── kanban.js           # Drag/drop board\n    ├── calendar.js         # FullCalendar-style CRUD\n    ├── settings.js         # localStorage-backed settings page\n    ├── form-controls.js    # Date range, multi-select, rich text\n    ├── file-manager.js     # Tree + grid file browser\n    ├── details.js          # Disclosure rows\n    ├── markup.js           # HTML pretty-printer for component playground\n    ├── data-adapter.js     # Demo data shim\n    ├── product-images.js   # E-commerce gallery\n    └── product-mockups.js  # Storefront demo\n\nproduction/                 # 58 HTML entry pages (auto-discovered)\npublic/                     # Static assets copied verbatim to dist/\ntypes/gentelella.d.ts       # TypeScript declarations for the public JS surface\nscripts/\n├── new-page.mjs            # Scaffold a page + register in NAV\n├── screenshots.mjs         # Playwright capture (22 pages × 2 themes)\n├── smoke.mjs               # Boot dev server, fetch every page\n└── deploy-preview.sh       # Build + R2 sync + cache-header pass\n\nexamples/                   # Standalone integration examples (Express/SQLite, etc.)\n```\n\n### Adding a new page\n\nUse the scaffolder — it writes the HTML, sets the body attributes correctly, and (optionally) inserts the page into NAV:\n\n```bash\nnpm run new -- reports --title \"Reports\" --nav-group \"Admin\"\nnpm run new -- user-roles --title \"User roles\" \\\n  --breadcrumb \"Home > User management|user_management.html > Roles\" \\\n  --nav-group \"Admin\" --icon profile\n```\n\nIf you write the file by hand instead, the contract is:\n\n1. Drop `production/<slug>.html`. Vite auto-discovers it (no config edit).\n2. Set `<body data-shell=\"admin\" data-page=\"<slug>\" data-breadcrumb=\"Home > …\">`.\n3. Add a `<script type=\"module\" src=\"/src/main-v4.js\"></script>` in `<head>`.\n4. To appear in the sidebar, edit `NAV` in [src/v4/shell-render.js](src/v4/shell-render.js) — match key to your `data-page`.\n\n### NAV and icons\n\nSingle 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`.\n\nIcons 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).\n\n### Breadcrumbs\n\n`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:\n\n1. **Explicit target** — `data-breadcrumb=\"Home > Projects|projects.html > Acme Redesign\"`. Everything after `|` is the href.\n2. **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.\n3. **Neither** — plain text, no link.\n\nLinks 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.\n\nPrefer 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.\n\n### Theming\n\nTokens 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.\n\nThe live theme generator at `production/theme.html` rewrites the same custom properties in real time and lets users copy/download the SCSS overrides.\n\n### Subpath deploys\n\n`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.\n\n### Service worker\n\nRegistered 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.\n\n## Conventions\n\n1. **Vanilla DOM only.** `querySelector`, `classList`, `addEventListener`. No jQuery shim, no SPA framework.\n2. **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.\n3. **Lazy import per-page modules** with a DOM-presence guard so the bundle never ships unused code:\n\n   ```js\n   if (document.querySelector('.calendar-grid')) {\n     import('./v4/calendar.js').then((m) => m.initCalendar());\n   }\n   ```\n\n4. **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.\n5. **`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).\n6. **`openMenu()` and `openPanel()`** ([src/v4/menus.js](src/v4/menus.js)) for any dropdown or slide-out — handles outside-click, escape, focus return.\n7. **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('--…')`.\n8. **ESLint single quotes + semicolons + 2-space indent.** Prettier formats. Both run pre-commit by convention; CI doesn't gate on them.\n9. **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.\n\n## Anti-patterns\n\n- Don't add jQuery, Bootstrap, or a SPA framework. v4's whole pitch is \"vanilla and small.\"\n- Don't hand-write Vite entry input lists — drop the file in `production/`.\n- Don't bypass `mountShell()` to wire up your own sidebar/topbar. Use `data-shell=\"admin\"` and let the plugin inject.\n- Don't hard-code `/` paths in HTML or JS. Use relative paths inside `production/*.html` and `import.meta.env.BASE_URL` in JS.\n- Don't import the whole of ECharts. The pattern in [src/v4/charts.js](src/v4/charts.js) does modular imports — match it.\n- Don't write directly to `dist/` — it's the build output, gitignored, blown away on every build.\n- Don't directly `new bootstrap.Modal(…)` — there is no Bootstrap. Use `showModal()`.\n- 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=`.)\n- Don't `Notification.objects.create()`-style direct DOM construction for toasts — use `showToast()`.\n- Don't edit files in `dist/`, `node_modules/`, or `docs/screenshots/` — they're all generated.\n\n## Recipes\n\n### New chart card\n\n1. Markup: `<div class=\"card chart-card\"><div class=\"chart\" data-chart=\"<id>\"></div></div>` inside your page.\n2. Add a `case '<id>':` in `initCharts()` in [src/v4/charts.js](src/v4/charts.js) that builds the ECharts `option` and returns it.\n3. The wrapper reads tokens via `getComputedStyle` so dark mode redraw is automatic.\n\n### New page in NAV\n\n1. `npm run new -- <slug> --nav-group \"<Group>\"` — done.\n2. 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`.\n\n### New modal or toast\n\n```js\nimport { showModal } from './v4/modal.js';\nshowModal({ title: 'Delete project?', body: 'This can\\'t be undone.', actions: [\n  { label: 'Cancel', variant: 'ghost' },\n  { label: 'Delete', variant: 'danger', action: () => { /* … */ } }\n]});\n\nimport { showToast } from './v4/toast.js';\nshowToast('Saved', { variant: 'success' });\n```\n\n### Wire up keyboard shortcuts\n\nSingle 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.\n\n## Build output\n\n```text\ndist/\n├── assets/         # Hashed CSS + fonts\n├── images/         # Hashed images\n├── js/             # Hashed JS chunks\n├── production/     # 58 entry HTMLs (paths resolved at build time)\n├── site.webmanifest\n├── sw.js\n└── stats.html      # Bundle analyzer (stripped by deploy script)\n```\n\nThe 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.\n\n## TypeScript\n\nNo `.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.\n","category":"root","tokens":3623}]}