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/.
npm install
npm start # http://localhost:5005
npm run dev # restarts on added/removed files
npm run build # outputs dist/
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
<body class="bg-bg text-text">
<include-sidebar />
<include-topbar />
<main id="main-content" class="pt-16 ml-0 lg:ml-64 transition-all duration-300">
<div class="mx-auto w-full max-w-[1400px] space-y-4 p-4 md:p-6">
<!-- page content -->
</div>
</main>
</body>
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.
// assets/js/components/my-page.js
export function initMyPage() {
const root = document.querySelector("[data-my-page]");
if (!root) return; // no-op on every other page
// …
}
// assets/js/index.js
import { initMyPage } from "./components/my-page.js";
document.addEventListener("DOMContentLoaded", () => {
initMyPage();
});
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.
window.addEventListener("Corepanel:theme-change", function (e) {
// e.detail.theme === "light" | "dark"
// re-read colours from the CSS tokens and repaint
});
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
Borders
border, -subtle, -strong
Status
success, danger, warning, info — each with -soft, -on-soft
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
Create
src/my-page.html. Copy the<head>— including the inline theme script — from an existing page. -
2
Add the chrome includes and the
<main>shell shown above. -
3
If it needs behaviour, add a component module exporting
initMyPage()with the no-op guard, then import and call it inindex.js. -
4
Add the sidebar link in both places.
partials/sidebar.htmlcontains two separate copies of the nav — mobile drawer and desktop rail. Editing one is the most common mistake in this codebase. -
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 classes —
bg-surface, notbg-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-startso 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) |