19 KiB
Development Guide
This guide covers the complete development workflow for the Aurora theme, from environment setup to building production packages.
Prerequisites
- Node.js v20.19+ - JavaScript runtime
- pnpm - Package manager (managed via Corepack)
- Tailwind CSS knowledge - Required for styling. See Tailwind CSS Documentation
- Network access - Development machine must be on the same network as your OpenWrt router
Environment Setup
1. Clone and Install
# Clone the repository
git clone git@github.com:eamonxg/luci-theme-aurora.git
cd luci-theme-aurora/.dev/
# Enable Corepack to manage pnpm version
corepack enable && corepack prepare
# Install dependencies
pnpm install
2. Configure Environment
# Copy environment template
cp .env.example .env
# Edit .env and set your OpenWrt device address
# VITE_OPENWRT_HOST=http://192.168.1.1
Environment Variables:
VITE_OPENWRT_HOST- Your OpenWrt LuCI web interface URL (required)VITE_OPENWRT_SSH_HOST- SSH target for.uttemplate sync, e.g.root@192.168.1.1(optional)VITE_OPENWRT_SSH_KEY- Path to SSH private key (optional, falls back to ssh-agent or~/.ssh/config)VITE_DEV_HOST- Development server host (code default:127.0.0.1,.env.examplesets0.0.0.0for LAN access)VITE_DEV_PORT- Development server port (default:5173)
Development Workflow
Start Development Server
cd luci-theme-aurora/.dev/
pnpm dev
The development server will start at http://127.0.0.1:5173 and proxy requests to your OpenWrt device.
How Vite Proxy Works:
The Vite development server uses middleware to rewrite local requests to serve CSS/JS resources from your development environment instead of the router. This enables live editing without deploying to the router. For detailed implementation, see vite.config.ts.
Key proxy behaviors:
- Proxies
/cgi-binand/luci-staticrequests to OpenWrt device - Uses middleware (
createLocalServePlugin) to rewrite request paths for CSS and JS files - CSS requests to
/luci-static/aurora/main.cssand/luci-static/aurora/login.cssare rewritten to serve from.dev/src/media/main.cssand.dev/src/media/login.cssrespectively - JS file requests are served directly from
.dev/src/resource/with middleware reading and returning file content - Injects Vite HMR client into proxied HTML responses for live reload support
- Redirects
/to/cgi-bin/lucifor proper routing
Code Style and Formatting
This project uses Prettier for code formatting with automatic formatting on save.
Prettier Configuration:
- Located in
.prettierrc - VS Code settings in
.vscode/settings.jsonenable format-on-save for CSS and JS files - Uses
prettier-plugin-tailwindcssto sort Tailwind CSS classes
CSS Nesting Support
Thanks to lightningcss, you can freely use CSS Nesting syntax in your stylesheets. The build process automatically compiles nested CSS into flat, browser-compatible format.
This will be compiled to standard CSS that works in all browsers.
CSS Architecture
The theme has two independent Tailwind CSS v4 entry points, both sourced from .dev/src/media/:
main.css— the LuCI admin UI. It contains no rules of its own; it's a pure import manifest that pulls in (in order)_tokens.css(OKLCH theme tokens, mapped via@theme inline),_base.css,_elements.css,_layout.css, every file incomponents/(one partial per UI component — buttons, cards, modals, tables, etc.), and_utilities.css.login.css— the standalone login page (sysauth.ut). Self-contained: imports Tailwind and_tokens.cssdirectly.
Third-party compatibility patches are not bundled into main.css — they are split into per-page files under media/patches/ and loaded on demand (see On-Demand Third-Party Patches below).
Adding new styles:
- New UI component → create
components/_<name>.cssand add an@importline tomain.css. Each file is its own organizational unit — no@layerwrappers needed (any that remain are stripped by PostCSS). - Compatibility fix for a third-party LuCI app/page → add a new file under
media/patches/(see below).
All rules use @apply with Tailwind utilities and CSS Nesting — no raw CSS properties.
On-Demand Third-Party Patches
Some third-party LuCI apps ship markup that doesn't adapt to the theme and needs a narrow compatibility override. Instead of bundling every such patch into main.css (which would ship them to every page), each patch is a standalone CSS file loaded only on the page it targets.
How it works:
- One file per page, named by
data-page. Each patch lives atmedia/patches/<page>.css, where<page>is the value of<body data-page="…">for the target page — i.e. the request path segments joined by-(e.g.admin-services-openclash-config).header.utcomputes the same string at render time fromctx.request_path, falling back toctx.pathwhenrequest_pathis empty (join('-', length(ctx.request_path) ? ctx.request_path : ctx.path)) so default landings reached without an explicit path still resolve their patch. - Build splits, not bundles.
vite.config.tsadds everymedia/patches/*.cssas its own Rollup entry, so each compiles tohtdocs/luci-static/aurora/patches/<page>.css. They are no longer part ofmain.css. @reference, not@import. Every patch file starts with@reference "../main.css";. This loads the theme context (tokens, custom utilities likebg-surface, thedark:variant) so@applyresolves — without re-emittingmain.cssinto the patch output. Using@importhere would inline all ofmain.cssinto every patch (hundreds of KB);@referencekeeps each patch to just its own rules.header.utlinks exactly one patch. A static allow-listPATCH_PAGESinheader.utlists which pages have a patch file. When the current page is in the list, header emits a single<link>to its patch file, right aftermain.css(so patches can override base styles). Pages not in the list get nothing — no extra request, no 404. A static list is used (rather than probing the filesystem) to avoid a hardfsdependency in the template and to avoid 404s on the ~95% of pages with no patch.
Adding a patch:
- Open the target page in the browser and read
document.body.dataset.page— that exact string is your filename. - Create
media/patches/<that-string>.css:/* PATCH: <page> (luci-app-foo) */ @reference "../main.css"; [data-page="<page>"] { /* narrow, selector-scoped overrides using @apply + CSS Nesting */ } - Run
pnpm build(or justpnpm gen:patch-pages). ThePATCH_PAGESallow-list inheader.utis auto-generated from thepatches/directory byscripts/gen-patch-pages.js— no manual editing. It rewrites the region between the//#patch-pages-start///#patch-pages-endmarkers; don't hand-edit inside them. - Verify
htdocs/luci-static/aurora/patches/<page>.cssis small (just your rules, not a copy ofmain.css).
Removing a patch is symmetric: delete the file and rebuild — it drops out of
PATCH_PAGESautomatically.
Naming notes: match the page exactly — granularity is per page, not per app. An app with several pages (e.g. openclash
…-configand…-settings) gets one file per page. The filename has no_prefix (unlike the_-prefixed partials, which are@import-only fragments); patch files are real build entries that ship tohtdocs/.
Design Tokens
src/media/_tokens.css is generated — its header says "DO NOT EDIT". The source of truth is .dev/tokens/:
defaults.js— the 10 editable input colors (bg,surface,text,brand,on_brand,link,info,warning,success,danger) for light and dark mode, as OKLCH strings.spec.js—DERIVATIONS(how every other token —text_muted,surface_sunken,hairline,brand_hover,brand_subtle,focus_ring,progress_start/progress_end,*_surface,scrim,mega_menu_bg, …) is computed from the inputs viamix/shade/set/alpha/constoperators, andFIXED(mode-specific literals such as shadows that bypass derivation).engine.js— the OKLCH/OKLAB color math behind those operators, via colorjs.io.resolve.js—resolveMode(mode)walksDERIVATIONSand returns a flat{token: oklchString}map with nocolor-mix()/var()left in it.
Changing a color:
- Edit
tokens/defaults.js(base input colors) and/ortokens/spec.js(derivation rules, fixed literals). - Run
pnpm gen:tokens(also runs automatically as part ofpnpm build) to rewritesrc/media/_tokens.css— it emits:root(light) and[data-darkmode="true"](dark) blocks plus the@theme inlinemapping, in that order. - Run
pnpm testto check the color-math operators and derived-token invariants (tests/engine.test.js,tests/resolve.test.js,tests/surfaces.test.js) — e.g. hue families, lightness ordering betweenbg/surface_sunken/surface, and translucency of menu backgrounds.
LuCI JavaScript API
For LuCI-specific JavaScript development, refer to the official API documentation:
Live Reload Behavior
- CSS changes: Trigger full page reload via custom HMR handler
- JS changes: Trigger full page reload via custom HMR handler
- Template changes (
.utfiles): Auto-synced to router via SCP and trigger full page reload (requires SSH setup, see below)
Template (.ut) Live Sync
The .ut template files are rendered server-side on the OpenWrt device. To see template changes during development, the dev server can automatically sync modified .ut files to the router via SCP.
1. Set up SSH key authentication to your router:
# Generate a key if you don't have one
ssh-keygen -t ed25519
# Copy your public key to the router (OpenWrt uses Dropbear, not OpenSSH)
cat ~/.ssh/id_ed25519.pub | ssh root@192.168.1.1 "cat >> /etc/dropbear/authorized_keys"
# Verify passwordless login works
ssh root@192.168.1.1 "echo ok"
2. Add SSH config to .env:
# SSH target for .ut file sync (user@host)
VITE_OPENWRT_SSH_HOST=root@192.168.1.1
# Optional: path to SSH private key (falls back to ssh-agent or ~/.ssh/config)
# VITE_OPENWRT_SSH_KEY=~/.ssh/id_ed25519
3. Start pnpm dev and edit any .ut file — the dev server will automatically sync it to the router and reload the browser.
Troubleshooting:
The dev server checks SSH connectivity on startup and prints actionable errors:
- Host key mismatch (device was reflashed): Run
ssh-keygen -R <device-ip>, then restart the dev server - Authentication failed (public key not on device): Copy your key with the command above
- Connection refused/timed out: Check that the device is online and SSH is enabled
If VITE_OPENWRT_SSH_HOST is not set, template sync is simply disabled and other dev features work normally.
Building for Production
Build Command
cd luci-theme-aurora/.dev/
pnpm build
This compiles all assets to the production directory htdocs/luci-static/, which is used by LuCI during OpenWrt package compilation.
Build Output:
htdocs/luci-static/
├── aurora/
│ ├── main.css # Minified admin UI CSS (via lightningcss)
│ ├── login.css # Minified login page CSS (via lightningcss)
│ ├── fonts/ # Web fonts (Lato)
│ └── images/ # Logo assets + PWA icons
└── resources/
└── menu-aurora.js # Menu configuration (minified via Terser)
Build Process:
pnpm gen:tokensregeneratessrc/media/_tokens.cssfromtokens/(see Design Tokens)- Vite builds the CSS entry points (
src/media/main.cssandsrc/media/login.css) - Custom PostCSS plugin removes
@layerat-rules for OpenWrt compatibility - Custom Vite plugin (
luci-js-compress) minifies JS files via Terser - Static assets copied from
.dev/public/aurora/
Package Compilation
Via GitHub Actions
Build frontend assets:
- Manually trigger the
frontend-assets-buildworkflow - It runs
pnpm build, then auto-commits the output tohtdocs/if anything changed
Build .ipk/.apk packages:
- Push a version tag (
v*), push tomaster/feat/**with[build]in the commit message, or manually trigger the workflow - The
build-theme-packageworkflow compiles both.ipkand.apkOpenWrt packages
PR review:
Pull requests that touch .dev/, htdocs/, ucode/, or root/ are automatically reviewed by the claude-pr-review workflow — it posts inline comments on the source diff (generated htdocs/ output is excluded) plus a summary comment. Mention @claude in a PR comment to request a follow-up review or ask a question.
Issue triage:
New issues are handled by the claude-issue-bot workflow — it checks for spam/duplicates, applies labels, and posts a deep technical analysis comment. Mention @claude in an issue comment to get a response.
Workflow Files: .github/workflows/
frontend-assets-build.yml— Build assets and auto-commit (manual trigger)build-theme-package.yml— Compile.ipk/.apkpackagesclaude-pr-review.yml— AI code review for PRs (inline + summary comments)claude-issue-bot.yml— AI issue triage and analysis
Directory Structure
luci-theme-aurora/
├── .dev/ # Development environment
│ ├── docs/ # Project documentation
│ │ ├── changelog/ # Version changelogs
│ │ └── DEVELOPMENT.md # Development guide (this file)
│ ├── public/aurora/ # Public static assets
│ │ ├── fonts/ # Web fonts (Lato)
│ │ └── images/ # Theme images + PWA icons
│ ├── scripts/ # Build scripts
│ │ ├── clean.js # Build cleanup utility
│ │ └── gen-tokens.js # Regenerates src/media/_tokens.css from tokens/
│ ├── src/ # Source code
│ │ ├── assets/icons/ # SVG icons
│ │ ├── media/ # CSS source (Tailwind CSS v4)
│ │ │ ├── main.css # Admin UI entry point (import manifest)
│ │ │ ├── login.css # Login page entry point
│ │ │ ├── _tokens.css # OKLCH theme tokens -- GENERATED, see tokens/
│ │ │ ├── _base.css # Document foundation (html/body viewport bg)
│ │ │ ├── _elements.css # Base element styles (headings, links, …)
│ │ │ ├── _layout.css # Page layout/structure
│ │ │ ├── _utilities.css # Custom utility classes
│ │ │ ├── components/ # One partial per UI component
│ │ │ └── patches/ # Per-page third-party patches (on-demand, one file per data-page)
│ │ └── resource/ # JavaScript resources
│ │ └── menu-aurora.js # Menu logic
│ ├── tokens/ # Design token source (-> src/media/_tokens.css)
│ │ ├── defaults.js # 10 editable input colors (light/dark)
│ │ ├── spec.js # Derivation rules (DERIVATIONS) + fixed literals
│ │ ├── engine.js # OKLCH/OKLAB color math (mix/shade/set/alpha)
│ │ └── resolve.js # Resolves spec into a flat token map
│ ├── tests/ # All test suites (pnpm test)
│ │ ├── engine.test.js # Color-math operators
│ │ ├── resolve.test.js # Resolved token invariants
│ │ ├── surfaces.test.js # Surface/hue layering invariants
│ │ ├── overlay.test.js # Overlay/layout CSS assertions
│ │ └── navigation-*.test.js # Navigation model/rendering/styles
│ ├── .env.example # Environment variables template
│ ├── .prettierrc # Prettier configuration
│ ├── package.json # Node.js dependencies
│ ├── pnpm-lock.yaml # pnpm lock file
│ └── vite.config.ts # Vite configuration with custom plugins
├── .github/ # GitHub configuration
│ ├── ISSUE_TEMPLATE/ # Issue templates
│ ├── workflows/ # GitHub Actions workflows
│ └── renovate.json # Renovate dependency update config
├── .vscode/ # VS Code workspace settings
│ └── settings.json # Auto-format on save settings
├── htdocs/luci-static/ # Build output (generated by Vite)
│ ├── aurora/ # Theme CSS and assets
│ │ ├── fonts/ # Built font files
│ │ ├── images/ # Built images + PWA icons
│ │ ├── main.css # Compiled admin UI CSS
│ │ ├── login.css # Compiled login page CSS
│ │ └── patches/ # Compiled per-page patches (linked on demand by header.ut)
│ └── resources/ # Built JavaScript modules
│ └── menu-aurora.js # Minified menu logic
├── root/etc/uci-defaults/ # OpenWrt system integration
│ └── 30_luci-theme-aurora # Theme auto-setup script
├── ucode/template/themes/aurora/ # LuCI ucode templates
│ ├── header.ut # Header template
│ ├── footer.ut # Footer template
│ └── sysauth.ut # Login page template
├── LICENSE # Apache License 2.0
├── Makefile # OpenWrt package Makefile
├── README.md # English documentation
└── README_zh.md # Chinese documentation
Tools and Technologies
- Tailwind CSS v4 - Utility-first CSS framework
- Vite - Build tool and development server
- pnpm - Fast, disk space efficient package manager
- lightningcss - CSS minifier
- colorjs.io - OKLCH/OKLAB color math for design token generation (
.dev/tokens/) - Terser - JavaScript minifier
- Prettier - Code formatter
- prettier-plugin-tailwindcss - Tailwind class sorting
- tw-animate-css - Animation utilities for Tailwind CSS
- tailwind-scrollbar - Custom scrollbar styling plugin