Building a theme
Scaffold a theme, author the ThemeSpec, validate it, build the .i99theme bundle, and publish to the catalog.
This is the end-to-end path for a theme, mirroring the mini-app flow.
You scaffold a project, edit a JSON design-token document, validate it,
build a .i99theme bundle, and publish it. ~10 minutes.
Prerequisites
| Tool | Version | Check |
|---|---|---|
| Node | ≥ 20 | node --version |
| pnpm | ≥ 9 | pnpm --version |
| i99dash CLI | ≥ 5.2 | pnpm dlx i99dash --version |
You also need a developer account for the publish step — the same login used for mini-apps. See Authentication.
Scaffold
pnpm dlx i99dash theme init my-theme
cd my-theme
pnpm installtheme init prompts you to pick a catalog category
(press a number or type a slug). Skip the prompt with --yes (defaults
to other) or pre-answer with --category neon. Pass --force to
scaffold into a non-empty directory.
You get:
my-theme/
├── package.json ← scripts: validate, build, publish
├── theme.json ← the ThemeManifest incl. the inline spec
├── .gitignore
├── assets/
│ └── icon.svg ← placeholder; replace with 256×256 artwork
└── wallpaper/
└── WALLPAPER.md ← how to add optional wallpapersThe generated theme.json is the canonical dark starter — all 8 surface
keys plus accent/secondary/error/warning/neutral are filled
in, and the id derives from the directory name.
Lay out the bundle
A theme is the project directory verbatim — there is no build step that
generates files, so every asset referenced by theme.json must already
exist in the tree at validate time. The .i99theme bundle (built in a
later step) packs exactly this layout:
theme.json ← ThemeManifest incl. inline spec (required)
icon.png ← tile icon (required)
cover.png ← optional 16:9 detail-surface cover
screenshots/*.png ← optional gallery (≤ 8)
wallpaper/
home.png ← optional
home-dark.png ← optional
cluster.png ← optional
fonts/*.ttf ← optional (only if spec.typography.bundled === true)Asset paths in theme.json are bundle-relative: they must start
with ./, may not contain .., and are ≤ 256 chars. The publish
service rewrites every path to an absolute HTTPS CDN URL at submit time
— identical to a mini-app's icon.
Author the ThemeSpec
Edit the spec object in theme.json. The colors block is where most
of the work is. Required keys:
- The 8 surface colors:
background,surfaceLow,surfaceContainer,surfaceHigh,outline,outlineVariant,onSurface,onSurfaceVariant. - The 3 brand/semantic colors:
accent,secondary,error.
Optional: warning, neutral (default to the car's built-in values),
plus the whole wallpaper, typography, shape, and gauge blocks.
{
"spec": {
"schema": 1,
"brightness": "dark",
"colors": {
"background": "#07070D",
"surfaceLow": "#0F1018",
"surfaceContainer": "#13141C",
"surfaceHigh": "#1A1C26",
"outline": "#4B5064",
"outlineVariant": "#24262F",
"onSurface": "#F3F4F8",
"onSurfaceVariant": "#8A90A4",
"accent": "#22D3A8",
"secondary": "#5B8CFF",
"error": "#E76F51"
},
"shape": { "cardRadius": 24, "buttonRadius": 14, "inputRadius": 14 }
}
}Every color is a hex string — #RRGGBB or #AARRGGBB. The full token
tables, with constraints and defaults, are in the
ThemeSpec reference.
The feature ships inert
Omitting wallpaper, typography, shape, and gauge reproduces the
car's default look exactly. Start from the colors and layer the rest on
only when you want to change it.
Validate
pnpm validate # → i99dash theme validatetheme validate is a fast (~150 ms, no network) pre-flight that:
- Zod-validates
theme.jsonagainst the canonicalThemeManifestschema, including the inlinespec. - Checks every declared asset (icon / cover / screenshots / wallpapers) exists, has an allowed extension, fits its size + dimension budget, and does not traverse outside the project.
Unlike a mini-app, a missing asset is fatal here — a theme has no
framework public/ step that could produce the file later, so it must
be present in the project tree.
Build the bundle
pnpm build # → i99dash theme buildProduces a deterministic .i99theme tarball at
dist/<id>-<version>.i99theme. "Deterministic" means the same inputs
always produce the same bytes (and therefore the same SHA-256), so the
backend can dedupe re-uploads by content hash. The build prints the
tarball path, byte size, and SHA-256. Override the output directory with
-o <dir>.
Publish
pnpm dlx i99dash login # one-time SSH-key sign-in; stores a token in your keychain
pnpm publish # → i99dash theme publishtheme publish runs the full pipeline: validate → build → request a
presigned upload URL → PUT the bytes to object storage → submit the
manifest against the uploaded bundle. It prints the review status when
done; a pending status means an admin review is queued, which you can
track with i99dash status.
Use --dry-run to validate + build the tarball without uploading —
handy in CI to prove a theme is publishable without minting bytes:
pnpm publish --dry-runUpdating after publish
Re-run i99dash theme publish with a bumped version. The version
busts the bundle/CDN cache, the same discipline as mini-apps. Never
rotate id — the car persists it as the user's active-theme selection,
so changing it orphans every install.