i99dash docs
Themes

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

ToolVersionCheck
Node≥ 20node --version
pnpm≥ 9pnpm --version
i99dash CLI≥ 5.2pnpm 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 install

theme 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 wallpapers

The 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 validate

theme validate is a fast (~150 ms, no network) pre-flight that:

  1. Zod-validates theme.json against the canonical ThemeManifest schema, including the inline spec.
  2. 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 build

Produces 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 publish

theme 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-run

Updating 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.

Next

On this page