PhoenixDevelopers

Making Phoenix themes

On this page

Who this is for: theme authors who want every manifest field, asset rule and limit in one place. New to themes? Start a theme first.

Build passive appearance packs for Jibo: ordinary menus, original eye paint and bounded layout choices. Start here, then use the detailed references:

Ember and Orbit are open, original examples for making your own Jibo menu theme. Four additional runnable starters live in tools/theme-kit/templates/:

StarterAPIDemonstrates
paper1Light palette, no images; native icon and geometry fallback
signals1Complete accepted icon map and generic declarative Store tile icon; native app identities remain native
terrace4Abstract background, per-menu fonts/palette, continuous bounded geometry
cut-paper3Original neutral step-bar paint on the native animated eye mesh

The kit builds unsigned data, not a robot skill, software image or executable. You need Node.js 20 or newer. PNG authoring has no npm dependencies: the CLI uses only Node built-ins and the files in tools/theme-kit/. Python 3 with Pillow is needed only for JPEG input; the optional art generator also needs NumPy.

A robot must advertise phoenix-menu-theme-v1, your exact theme API and a compatible minFirmware floor. A version number alone is not proof of support. Ember/Orbit retain their original minFirmware: "13.6.1"; the new starters use 13.7.1. Choose the lowest release you actually test with the required capability. Do not lower themeApi to bypass compatibility checks.

What a theme can change

  • Six palette colors, a static background, bounded bundled-font choices/sizes.
  • Static icon replacements for eligible ordinary menu items in the accepted map below. Native app identities and the runtime exceptions remain native.
  • A firmware-owned layout: native (paged buttons) or slide (sliding cards). The owner can choose a layout per robot and enable reduced motion.
  • An optional store preview and generic icon for declarative Store app tiles.
  • Original eye paint assets on Jibo's native animated mesh (API 3); API 4 adds closed/listening paint, a face background and bounded display scale.
  • Bounded button, icon and label sizing/offsets with paged or continuous scrolling (API 4). Jibo clamps geometry to its native safe area; this is not a view editor.

Full decoration is supported on the Main, Fun, Personal Report, Clock, Create, Exercise and ordinary Settings menus. Wi-Fi submenus accept palette, background and fonts, not replacement icons or layout. Other ordinary menus may receive limited cosmetics according to firmware policy; a pack cannot expand coverage. Unsupported or specialized menus keep native controls/geometry.

Themes cannot change actions, labels, menu order, navigation, STOP/swipe-down behavior, speech, camera, network access or permissions. API 1–3 choose native layout presets, not authored target dimensions; API 4 adds only bounded geometry described in the reference. Safety rows (Wipe, Shutdown, Firewall) keep their button colours, icon identity, label wording and actions, but their label font/colour and eligible menu geometry can change. Stateful network/battery indicators, confirmation colours, update/recovery screens and the Default/picker controls stay native. Tutorial guided screens, Gallery custom views, chat, Web Panel, photos, clock faces, volume sliders, restore and setup screens are outside the menu API. Eye paint is a separate capability, not permission to replace these views.

No JavaScript, actions, raw BE view configs, MIMs, timelines, downloaded fonts, CSS, HTML, shaders, sounds, SVG or external URLs are accepted. Backgrounds and icons are passive images; layout motion comes from firmware, never an asset. Do not depict Jibo's robot, shell, head, face or eye in the artwork.

Start from Ember or Orbit

Run these commands from the Phoenix checkout:

node tools/theme-kit/cli.mjs check themes/ember
node tools/theme-kit/cli.mjs check themes/orbit
node tools/theme-kit/cli.mjs new copper --from ember --author "Your name"
# Or use one of the capability-focused starters:
node tools/theme-kit/cli.mjs new notebook --from paper --author "Your name"
node tools/theme-kit/cli.mjs new wayfinding --from signals --author "Your name"
node tools/theme-kit/cli.mjs new hillside --from terrace --author "Your name"
node tools/theme-kit/cli.mjs new folded --from cut-paper --author "Your name"
# Orbit starts with sliding cards:
node tools/theme-kit/cli.mjs new starlight --from orbit --author "Your name"

new creates <current-directory>/<name>/ and never overwrites an existing path. Names are 1–40 lowercase letters/digits/hyphens beginning with a letter. It copies the art, palette/layout/role map and original license, changes the manifest name and description, records your name in AUTHOR.md, and creates a fresh local app ID placeholder. --author is optional; without it the credit says Your name. Author text is 1–100 single-line characters. Replace all author placeholders.

Before submission, create your own app identity in Publish and replace appId with that publisher-assigned ID. A generated UUID is not a registered Store identity. Do not publish a derivative using Ember's or Orbit's app ID. There is no author manifest field: publisher ownership is checked by the store; AUTHOR.md is host-side attribution, not a runtime permission or signed manifest field.

Edit theme.json and the images in your copy. README.md, AUTHOR.md and LICENSE.md are never packed; only the manifest and its listed images are used. A theme may omit icons/background/preview entirely and provide only a palette: use assets: [], background: {"asset": null, "opacity": 0}, overrides: [], and omit previewAsset and storeAppIcon.

Author manifest: theme.json

JSON is strict: unknown/missing fields, duplicate keys, malformed UTF-8, prototype keys (__proto__, prototype, constructor), nonfinite/unsafe numbers and trailing data are rejected. Maximum depth is 12; an object or array has at most 64 members. Text is plain text, not markup or expressions. Lengths are JavaScript string lengths (UTF-16 code units). Required text cannot be empty/whitespace-only; single-line text excludes all control characters, and long text permits only tabs and line breaks among controls.

All top-level fields below are required:

FieldContract
schema / kindExactly 2 / "theme"
runtime / themeApiRuntime is exactly "phoenix-menu-theme-v1"; API is 1, 2, 3 or 4 according to the extension fields
appIdapp- plus a lowercase UUID in 8-4-4-4-12 hex groups; must belong to the submitting publisher
versionPlain X.Y.Z; components are nonnegative safe integers, no leading zeros or prerelease/build suffixes; published versions are immutable and must increase
minFirmwareThe same plain X.Y.Z grammar; select a supported theme-capable firmware floor
nameNonempty single-line text, at most 60 characters
descriptionNonempty long text, at most 1,200 characters
releaseNotesLong text, at most 1,200 characters; empty string is allowed
permissionsExactly ["menu-theme"]
launchNamesExactly []; themes are not launchable apps
themeThe object described below
assetsAt most 64 source entries, each exactly { "id", "role", "path" }

The theme object

This table describes the base object, required at every API. Optional eye, eyeAsset, face and menuLayout groups, including per-menu geometry, are fully specified in the extension reference.

FieldRequired?Contract
layoutYes"native" or "slide"
paletteYesAll six #RRGGBB fields below
fontsYesExactly titleFamily, labelFamily, titleSize, labelSize
backgroundYesExactly asset (background ID or null) and opacity (finite number 0–0.35)
previewAssetNoAn existing asset ID with role preview; omit if not supplied, do not set to null
storeAppIconNoAn existing asset ID with role icon, for declarative Store tiles only
overridesYesArray of at most 32 objects; each menuKey must be unique

fonts.titleFamily and fonts.labelFamily are each one of Proxima Nova Soft, Proxima Nova Light, Proxima Nova. Title size is an integer 48–100; label size is an integer 40–56. These select fonts already bundled in firmware, not font files included in this repository or permission to redistribute those fonts.

Each override requires menuKey, and may contain layout, palette, fonts, background, icons. An omitted group inherits the global group; a supplied palette/font/background group must contain all its fields, not a partial patch. icons is an array of at most 64 entries, each exactly { "itemId", "asset" }. Item IDs are unique within an override, 1–80 letters/digits/underscores/hyphens, and must match the semantic allowlist. Menu keys are at most 120 characters and must match an exact allowed scope; syntactically valid unknown scopes still fail.

Allowed scopes:

MenuExact menuKeyAllowed overrides
Main@be/main-menu#main-menuPalette, fonts, background, layout, listed icons
Fun@be/main-menu#fun-stuffSame
Personal Report@be/main-menu#personal-reportSame
Clock@be/clock#clockMenuSame
Create@be/create#createMenuSame
Exercise@be/exercise#exercise-menuSame
Settings@be/settings#settingsMenuSame, ordinary rows only
Wi-Fi@be/settings#wifiMenuPalette, fonts, background only
Saved networks@be/settings#yournetworksPalette, fonts, background only
Network action menu@be/settings#networkNamePalette, fonts, background only

Palette and contrast

TokenUse
backgroundSolid backing behind menu decoration
surfacePrimary button/card backing
surfaceSecondarySecondary button/card/label backing
textOrdinary labels and titles
mutedTextRequired contrast-checked token; not a separate ordinary-menu text paint slot in the current renderer
accentFocus/border accent, not semantic danger/status colors

Every token is exactly a six-digit hex color (# plus six hex digits), without alpha, named colors or shorthand. Both text and mutedText must have WCAG relative-luminance contrast at least 4.5:1 against each of surface, surfaceSecondary, background. accent must be at least 3:1 against surface. These checks apply to the global palette and every override palette; invalid palettes fail rather than being silently adjusted. Preview/GPU/physical contrast still needs visual testing: a solid-color calculation is not a hardware or accessibility certification.

Images, paths and budgets

RoleRecommended example sizeMaximum width × heightAccepted author format
icon256 × 256, transparent padding256 × 256Static PNG
background1280 × 7201280 × 720Static PNG or baseline RGB JPEG
preview720 × 720720 × 720Static PNG or baseline RGB JPEG

Smaller positive dimensions are valid. The examples ship exact recommended sizes; icons have centered drawings within 160 × 160 on a transparent RGBA canvas. This drawing/padding convention is checked by the example art generator, not by the general packer. Keep important background content away from clipped corners of the physical circular display. Previews illustrate the menu without user data or a robot depiction; they are not hardware screenshots.

LimitMaximum
Source theme.json and normalized canonical manifest65,536 UTF-8 bytes each
Complete normalized .phtheme8,388,608 bytes (8 MiB)
Assets64, with at most one background and one preview
Each input and normalized image1,048,576 encoded bytes (1 MiB)
Total pixels across all assets4,194,304 (up to 16 MiB decoded RGBA)
Installed themes on a robot8, sharing the Store's overall cap of 40 installed apps
Robot theme disk allocation83,886,080 bytes (80 MiB), including candidate/retained predecessor; actual admission may be tighter
Resident decoded theme textures16,777,216 bytes (16 MiB) across renderer candidates, separate from native caches; images can be deferred under pressure

Both examples contain 40 images (38 icons + background + preview) and declare 3,930,368 pixels. Having room under the encoded-byte budget does not waive the pixel budget. Robot-advertised limits and publisher quotas may be lower; local validation does not guarantee store or robot storage admission.

Asset IDs are unique, begin with a lowercase letter, and have at most 40 lowercase letters/digits/hyphens. Author entries have no hashes or size metadata:

{"id": "weather", "role": "icon", "path": "assets/icons/weather.png"}

Paths are relative to the theme directory, either assets/<name>.png|jpg|jpeg or assets/icons/<name>.png|jpg|jpeg, with the same bounded lowercase name grammar. They must be regular files. No symlinks (including directory components), absolute paths, traversal or URLs. Every asset must be used by a correctly typed reference; unused images, missing references and duplicate IDs/scopes/items fail.

PNG must be static, noninterlaced, 8-bit RGB or RGBA. Indexed/grayscale PNG, APNG, GIF, WebP, SVG, progressive/grayscale JPEG and arbitrary metadata are rejected. Allowed PNG chunks are IHDR, IDAT, IEND and sRGB/gAMA/cHRM/pHYs; export without text/EXIF chunks. JPEG permits baseline RGB with JFIF only, not comments/EXIF/ICC or other metadata. Renaming a file does not convert its format. CRCs, image structure and all PNG scanline filters are checked; malformed, truncated or trailing image/compressed-stream payloads fail. JPEG is decoded by a bounded Python/Pillow child with a 10-second deadline.

Full icon role table

All 38 files use role icon, 256 × 256 recommended (maximum), with transparent padding and a centered 160 × 160 drawing. Filenames below are the examples' assets/icons/<stem>.png; derivatives may choose other valid IDs/filenames while preserving exact menuKey/itemId targets. IDs are case-sensitive.

StemMenu and exact item IDSize
introductionMain introduction256 × 256
things-to-doMain thingsToDo256 × 256
funMain fun256 × 256
clockMain clock256 × 256
reportMain personalReport; Personal Report fullReport256 × 256
musicMain radio256 × 256
cameraMain snapshot; Create photo256 × 256
galleryMain gallery256 × 256
photoboothMain photobooth; Create photobooth256 × 256
yogaMain exercise256 × 256
tutorialMain tutorial tile only, not guided screens256 × 256
settingsMain settings256 × 256
agentMain phoenix-agent, only if firmware already permits it256 × 256
spotifyMain trusted injected phoenix-spotify256 × 256
tic-tac-toeAccepted Main phoenix-tic-tac-toe mapping; native art retained (see below)256 × 256
apptheme.storeAppIcon, declarative Store app tiles only256 × 256
circuit-saverFun circuitSaver256 × 256
word-of-dayFun wordOfTheDay256 × 256
jokeFun joke256 × 256
danceFun dance256 × 256
surpriseFun surprise256 × 256
weatherPersonal Report weather256 × 256
calendarPersonal Report calendar256 × 256
commutePersonal Report commute256 × 256
newsPersonal Report news256 × 256
timeClock time256 × 256
dateClock date256 × 256
timerClock timer256 × 256
alarmClock alarm256 × 256
yoga-sunExercise sunSalutation256 × 256
yoga-warriorExercise basicWarrior256 × 256
volumeSettings volume256 × 256
wifiSettings wifi root tile, not signal/connect/delete controls256 × 256
batterySettings battery root tile, not live status256 × 256
aboutSettings about256 × 256
updatesSettings updates root tile, not progress/errors256 × 256
home-assistantSettings homeAssistant root tile256 × 256
themesSettings firmware themes tile256 × 256

Accepted targets and visible replacements

The table is the accepted manifest map, not a guarantee that every target is drawn from theme art. BE 13.7.1 accepts phoenix-tic-tac-toe in a pack, but it is not a renderer replacement target; that icon remains native. The Signals starter keeps this accepted mapping as a format example.

A trusted native app icon takes precedence over theme art even when its menu item ID is in the table. This includes native-v2 tiles and settings entries. storeAppIcon applies only to eligible declarative Store tiles, not native apps. An accepted target that is absent from the current menu does not add an item. Verify actual visible icons on the intended software release before promising coverage; valid format and rendered coverage are different checks.

Check, pack and regenerate

node tools/theme-kit/cli.mjs check copper
node tools/theme-kit/cli.mjs pack copper --out out/copper-v1
node --test tools/theme-kit/*.test.mjs

check fully validates and normalizes in memory without writing files. It reports SHA-256 digests, canonical manifest/artifact bytes, asset/pixel counts and global palette contrast. Override palettes are validated too. pack performs the same checks before creating output and emits:

  • copper-1.0.0.phtheme: the unsigned upload container.
  • copper-1.0.0.manifest.json: the exact canonical runtime manifest.
  • copper-1.0.0.preview.png: normalized preview, only if previewAsset is present.

The stem comes from the source directory name (or theme if not a valid stem). Existing output files are never overwritten: use a fresh output directory or remove old outputs deliberately. Nonzero exit means rejection; diagnostics name the failing rule/limit. Neither command signs, contacts a store or installs on a robot. Published themes use the Skill Store flow below. This kit has no SSH transport and cannot directly install its unsigned output on a robot.

The packer sorts assets lexically by ID, decodes accepted images, and emits clean RGB/RGBA PNGs with filter 0 and compression level 9. It derives each image's hash, bytes and dimensions; source paths never enter the runtime manifest. Canonical JSON recursively sorts object keys, preserves array order and uses compact UTF-8 JSON with no whitespace/newline. The container is not a zip/tar archive:

BytesMeaning
0–7Exact eight-byte ASCII magic PHXTHM1\n
8–11Unsigned 32-bit big-endian canonical manifest byte length
12 onwardCanonical UTF-8 manifest, then raw image blobs in asset order

Length equals 12 + manifestBytes + sum(asset.bytes). Packed asset entries are exactly {id, role, mediaType, sha256, bytes, width, height}. There are no filenames, archive extraction paths, nested compression or executable entries. The uploaded artifact hash covers the entire container; the manifest hash covers its exact JSON.

Committed reference hashes in tools/theme-kit/expected-sha256.json pin both examples against the reference packer. Equality was measured on Node 22.22.0, zlib 1.3.1-470d3a2, Pillow 12.3.0 and NumPy 2.2.6. Exact compressed bytes are not promised across arbitrary zlib/Pillow versions; use the pinned toolchain to reproduce the hash vectors. Pixel equivalence is a separate generator check:

python3 themes/build-art.py --check
python3 themes/build-art.py --check --sheets out/theme-review
# This REWRITES both original example asset sets:
python3 themes/build-art.py

See themes/README.md for dependency/regeneration guidance. Adapt a copy of the generator for your derivative, not the original pack paths. Review all art on its intended surfaces; a valid pack can still be hard to read.

Submit to the Skill Store

  1. Sign in and open Publish in the Skill Store: the publisher dashboard. Apply for publisher approval if needed; approval is required to upload. The publishing guide walks through every step.
  2. Use Create an app identity for this theme. Copy the returned app-… ID into your derivative manifest. Use a new identity, not an already-published declarative app: an existing app cannot silently change listing kind.
  3. Set your name, description, license/attribution and release notes, then check and pack again. Increase version for each update; versions cannot be reused.
  4. Submit the .phtheme to the theme publisher upload, not the declarative .json upload. Choose public visibility separately when ready; uploading alone does not imply public community listing or apply anything to a robot.

The authenticated publisher contract is:

POST /api/skill-store/publisher/theme-releases
Content-Type: application/vnd.phoenix.menu-theme
Body: raw .phtheme bytes (not JSON, base64 or multipart)

It requires your same-origin publisher session, an app identity owned by you, no content encoding, and a body of at most 8,388,608 bytes. Upload idle/absolute deadlines are 8/90 seconds. Default publisher limits include two concurrent uploads, six admissions/releases per hour and 32 MiB storage; invalid uploads also consume admission budget. Policy may tighten these limits.

Use Publish a theme on the Publish page. Choose or drop a .phtheme, review its identity, version and appearance summary, then use Publish version …. The page also accepts a ZIP of a source theme folder and packs it locally. Prefer the CLI-produced .phtheme for reproducible checks. Do not rename it to .json, and do not use the declarative app uploader. See publishing and updates for the full release checklist, ZIP limits and refusal handling.

The store repeats validation/normalization and keeps the result as an immutable pending submission, not a release. A successful response returns that submission: check its app ID, version and state. An operator reviews the exact source and normalized bytes; only an approved submission is signed and published, and the store creates the release ID and signature at that point. Authors do not need signing keys and must not hand-author signatures. Refresh your listing before changing visibility. See how review works. Publisher approval and operator review are not proof that every theme is safe, has rights clearance or renders correctly on all robots. Automated format/size/ contrast checks do not substitute for content review or hardware acceptance. A signature authenticates origin/integrity, not a hardware certification badge; hardware acceptance can remain unverified.

Test on a robot and recover

Install through the Store onto a compatible, owner-linked robot after reviewing the appearance consent: “Changes ordinary menu appearance and, when declared, eye colour or texture on this Jibo. Does not change actions, speech, camera or network access.” Install is not apply. Select it explicitly in Settings → Themes or the console's per-robot Appearance panel. Treat Waiting as pending, not active: the console reflects robot-reported Applied, Reverted to Default, Failed or Conflict outcomes.

Check Main/Fun/Personal Report/Clock/Create/Exercise/Settings, all icon roles, long labels, focus/readability, gestures and both layouts. Verify protected safety row semantics, status controls, Default recovery and ordinary app behavior. Safety row fonts and eligible layout may change; their labels/actions must not. The preview PNG and check cannot prove display clipping, GPU decoding or physical touch/voice behavior; test those on the intended hardware separately.

Default is built into firmware and requires no theme download. An invalid or corrupt pack is rejected; unsupported/missing target overrides keep native appearance, and missing icons/backgrounds have bundled-icon/solid-backing fallbacks. Undo or trial timeout restores the previous selection. A candidate that fails before commit leaves the prior menu appearance in place and restores native eye paint; a committed renderer failure restores Default. An unfinished trial on startup is rolled back. Themes are never loaded as code. Default/picker recovery controls are outside the theme's control; use Default theme to turn off decoration, including offline. Updates install a new release but do not silently change active visuals. Removing an active theme or explicit ownership revocation resets appearance to Default. These are firmware safety behaviors, not a claim that this authoring kit prevents OS/GPU/power failures.

Licensing and redistribution

The original Ember/Orbit LICENSE.md files explicitly license the artwork under BSD-3-Clause. This kit preserves that grant rather than relicensing to CC BY. The manifests, generator and standalone CLI also use BSD-3-Clause, like Phoenix. Keep the copyright notices, conditions and disclaimer, including with binary redistributions; do not imply endorsement. The pack excludes README/LICENSE/ AUTHOR files, so distribute those notices alongside source/artifact copies. Document your own changes and permissions in AUTHOR.md; publisher identity is not a substitute for attribution. No firmware, bundled font files or private store internals are included in this kit.