Documentation

How Corepanel is put together — the build, the design tokens, and the conventions to follow when adding to it.

Getting started

Requires Node.js and npm. Install once, then run the dev server.

npm install
Install dependencies.
npm start
Dev server with hot reload on localhost:5005.
npm run dev
Same, but also restarts when files are added or removed.
npm run build
Production build into dist/.
Terminal

dist/ is generated. Edit files in src/ only, then rebuild — anything written directly into dist/ is wiped on the next build.

Project structure

All source lives under src/. There is one stylesheet and one JS entry point.

src/
├── *.html               72 pages, one file per route
├── partials/
│   ├── sidebar.html     desktop rail + mobile drawer (two nav copies)
│   ├── topbar.html      search, theme, notifications, user menu
│   └── footer.html
└── assets/
    ├── css/index.css    the entire stylesheet: tokens + components
    ├── js/
    │   ├── index.js     single entry — imports and calls every init*()
    │   └── components/  47 per-feature modules
    └── images/

Pages & partials

Every *.html directly under src/ is discovered automatically and emitted to dist/. To add a page, create the file — no config change needed. Restart the dev server so it is picked up.

Shared chrome is injected at build time with self-closing tags. Because injection happens at build time, editing a partial requires a rebuild.

  • <include-sidebar /> → partials/sidebar.html
  • <include-topbar /> → partials/topbar.html
  • <include-footer /> → partials/footer.html
Page shell

JavaScript

assets/js/index.js is the only entry point. It imports every component module and calls each init*() inside one DOMContentLoaded listener. Every module loads on every page, so each init*() must no-op when its markup is absent.

The standard guard is an early querySelector probe. Without it, a module throws on the 71 pages that do not contain its markup.

Component module

Theming

Theme and sidebar state persist in localStorage. A small inline script in each <head> reads them before first paint, so there is no flash of the wrong theme. That is the one place inline JS is intentional — it has to run before the bundle loads.

Key Values
Corepanel-theme "light" | "dark"
Corepanel-sidebar "collapsed" (or absent)

Changing the theme dispatches a window event. Any component that caches a computed colour — charts especially — must subscribe, or it keeps the old palette until reload.

Theme event

Design tokens

Colours are CSS custom properties declared in the Tailwind v4 @theme block and re-declared under .dark. Use the token utilities — raw palette classes like bg-white will not respond to the theme.

Brand

primary, -soft, -strong, 50–900, secondary

Surfaces

bg, surface, subtle, w1, overlay

Text

heading, text, text-secondary, muted, faint

Aa Aa Aa Aa

Borders

border, -subtle, -strong

Status

success, danger, warning, info — each with -soft, -on-soft

Success Danger Warning

Charts

chart-1 … chart-6

Two contrast rules, both established by measurement:

  • -soft needs -on-soft. The base status colours are tuned for icons and fills; as small text on their own tint they land near 3:1.
  • Chart tokens fail AA as small text. chart-2/-3/-4 measure 3.68–4.23 against white. For legends use a neutral label with a coloured dot, not a coloured label.

Breakpoints

Custom, and not Tailwind's defaults — lg is 992px, not 1024px. The sidebar switches from mobile drawer to desktop rail at lg.

xs sm md lg xl 2xl 3xl 4xl
400px 576px 768px 992px 1200px 1400px 1600px 1800px

Adding a page

Five steps. The fourth is the one people forget.

  1. 1

    Create src/my-page.html. Copy the <head> — including the inline theme script — from an existing page.

  2. 2

    Add the chrome includes and the <main> shell shown above.

  3. 3

    If it needs behaviour, add a component module exporting initMyPage() with the no-op guard, then import and call it in index.js.

  4. 4

    Add the sidebar link in both places. partials/sidebar.html contains two separate copies of the nav — mobile drawer and desktop rail. Editing one is the most common mistake in this codebase.

  5. 5

    Restart the dev server so the new file is discovered, or run npm run dev.

Gotchas

Real traps in this codebase. Each has caused a bug.

Component classes beat responsive utilities

.topbar-icon-btn { display: inline-flex } is a plain rule that sits later in the stylesheet than Tailwind's utilities, so at equal specificity it wins on source order. The utility compiles but has no effect. Put the breakpoint utility on a wrapper instead.

<!-- BROKEN: sm:hidden never applies -->
<button class="topbar-icon-btn sm:hidden">…</button>

<!-- CORRECT: utility on a wrapper -->
<span class="sm:hidden"><button class="topbar-icon-btn">…</button></span>

Also affects .brand-logo (display) and .topbar-search (position).

Two copies of the sidebar nav

The mobile drawer and desktop rail are separate markup with slightly different classes. Every nav change must be made twice.

stack-table is not general-purpose

It collapses rows into cards below md, which suits wide tables (8+ columns). On a narrow table it produces a worse layout than letting the table scroll. For those, wrap in overflow-x-auto with a min-w-* on the table.

Viewport breakpoints inside narrow columns

sm:grid-cols-3 fires at a 576px viewport, even when the element sits in a 200px column. Inside a card that can be narrow, use container queries — @container on the parent, @[16rem]: on the child.

Shared attribute hooks are global

[data-switch] is bound app-wide by dashboard.js, [data-tabs] by ui-tabs.js. Do not add a second listener for the same attribute in a page module — two handlers on one click cancel each other out.

A CSV export BOM must be the escape

Excel needs a byte-order mark to read UTF-8 CSV, written as the escape sequence "\uFEFF". A literal BOM character pasted into the source decodes as three characters and shows up as . It hides from grep and from a file reader — only a hex dump tells them apart.

Conventions

What every page in this template is held to.

  • No inline CSS. The only intentional inline script is the pre-paint theme block.

  • No gradients, colour strips, or decorative bars. Colour carries meaning or it is not used.

  • Tokens over raw palette classesbg-surface, not bg-white.

  • Both themes pass WCAG AA — 4.5:1 body text, 3:1 large text and UI.

  • Cards leave no trailing empty space. Use items-start so each card sizes to its own content.

  • No horizontal page overflow at any width from 360px up. Wide content scrolls inside its own container.

Tech stack

Purpose Library
Styling Tailwind CSS v4
Bundling Webpack 5
Charts ApexCharts
Carousel Swiper
Lightbox GLightbox
Icons Phosphor Icons
Type Inter (self-hosted)