Make a theme with your agent
A reference for an AI agent that makes a MarsDawn preview theme. The agent writes one theme.json, checks and renders it with the marsdawn command-line tool, and hands you a link. You tune it by eye on the Build a theme page and submit it. The field tables below are read from the kit's own source, and a check keeps them equal to it.
Write one theme.json, run marsdawn theme validate theme.json --require-complete --json until it exits 0, render it with marsdawn theme preview in light and dark, then give the person a link. A theme is parameters only. It never contains CSS.
What a theme is
- Colours for light and for dark: a base palette, and optional syntax and diagram colours. Each is
#RRGGBB. - A font design:
sans,seriforrounded. - A closed set of style options: numbers inside fixed ranges, and choices from fixed lists. A colour in a style option is always a palette role such as
accent, never a literal colour. - Display strings: a name and a summary, and optionally an author and a licence.
A key that is not in the tables below is an error, and so is a key written twice. The file is at most 16,384 bytes.
Check and render
Run these in the folder that holds theme.json. They need the marsdawn tool: see marsdawn for agents.
marsdawn theme validate theme.json --require-complete --json
marsdawn theme css theme.json --json
marsdawn theme preview theme.json --appearance light -o light.png
marsdawn theme preview theme.json --appearance dark -o dark.png
- Exit codes: 0 valid, 1 invalid with rule ids, 2 file not found, 64 usage error.
previewadds 4 when the PNG already exists (pass--forceor choose another name) and 5 when rendering fails. - With
--json, an invalid theme lists its problems underissues, each with aruleid, apathand amessage. Fix them and run it again. --require-completeis the check for a theme that will be published: it also requires thesyntaxanddiagramcolours in both palettes.cssprints the CSS the preview serves.previewrenders a sample document to a PNG. Both validate first, so an invalid theme renders nothing.- Look at the light and the dark PNG before handing the theme over.
Start from a built-in
Copy one of the four built-in themes and change it. Each is a complete, valid file:
/assets/theme-sim/kit/themes/dawn.json: the default, and the example below/assets/theme-sim/kit/themes/classic.json: a plainer, document-like palette/assets/theme-sim/kit/themes/modern.json: a cooler, more contemporary palette/assets/theme-sim/kit/themes/vivid.json: a brighter, higher-contrast palette
Example: Dawn
This is dawn.json, byte for byte. Its only style option is a thin accent rule for hr.
{
"schemaVersion": 1,
"id": "dawn",
"version": "1.0.0",
"name": { "en": "Dawn" },
"summary": { "en": "Warm Martian sunrise" },
"fontDesign": "sans",
"scenarios": ["agent-review"],
"light": {
"background": "#FFFDFB", "surface": "#F6F0EC", "text": "#26211F", "muted": "#6F6660",
"border": "#EADFD8", "heading": "#26211F", "accent": "#C8471B", "link": "#B03C0C", "quote": "#D97A4A",
"syntax": { "keyword": "#B03C0C", "string": "#2F6F5E", "comment": "#746B66", "number": "#9A5B00", "function": "#5B3E8C", "type": "#1F6FB2" },
"diagram": { "node": "#FBE9E0", "nodeBorder": "#CA6C3C", "text": "#26211F", "line": "#8A6A5C", "secondary": "#E6F1EE", "tertiary": "#FFF6F1", "note": "#FFF1D6" }
},
"dark": {
"background": "#1C1A1F", "surface": "#262229", "text": "#EBE4DF", "muted": "#A39992",
"border": "#3A343A", "heading": "#F4EEEA", "accent": "#FF8A50", "link": "#FF9E6B", "quote": "#E08A5C",
"syntax": { "keyword": "#FF9E6B", "string": "#7FD1B9", "comment": "#938A84", "number": "#F2C572", "function": "#C7A6F5", "type": "#6CB6FF" },
"diagram": { "node": "#3A2A26", "nodeBorder": "#E08A5C", "text": "#EBE4DF", "line": "#B8A69C", "secondary": "#23332F", "tertiary": "#2B2427", "note": "#3D3322" }
},
"style": {
"hr": { "style": { "type": "line", "color": "accent", "thickness": 1 } }
}
}
Fields
Every object below is read the same way. required means the key must be present, and a key that is not listed is an error.
Types: string, integer, number, boolean, map (language tag to text), enum (one of the listed values), role (a palette role) and object (the table it links to). A [] after a type means a list.
theme.json
| Field | Type | Presence | Values and notes |
|---|---|---|---|
schemaVersion | integer | required | Always 1. |
id | string | required | 1 to 32 characters: lowercase letters and digits, in runs joined by single hyphens, such as calm-green. |
version | string | required | MAJOR.MINOR.PATCH, such as 1.0.0. |
name | map | required | Language tag to text. en is required; other keys are language tags such as zh-Hant. Each text is at most 48 characters, with no links or line breaks. |
summary | map | required | The same shape as name, at most 120 characters per text. |
fontDesign | enum | required | sans, serif, rounded |
scenarios | enum[] | required | agent-review, technical-docs, formal-output, notes-sharing — One or two, no repeats. They set how much contrast the theme must reach. |
author | object | optional | author |
license | string | optional | An SPDX identifier, such as Apache-2.0. Submitted themes are released under Apache-2.0. |
light | object | required | light, dark |
dark | object | required | light, dark |
style | object | optional | style |
author
| Field | Type | Presence | Values and notes |
|---|---|---|---|
name | string | required | At most 64 characters, with no links. |
github | string | optional | A GitHub login. For a submission, set it to the submitter's own. |
light, dark
| Field | Type | Presence | Values and notes |
|---|---|---|---|
background | string | required | # and six hex digits, such as #1C1C1C. |
surface | string | required | # and six hex digits, such as #1C1C1C. |
text | string | required | # and six hex digits, such as #1C1C1C. |
muted | string | required | # and six hex digits, such as #1C1C1C. |
border | string | required | # and six hex digits, such as #1C1C1C. |
heading | string | required | # and six hex digits, such as #1C1C1C. |
accent | string | required | # and six hex digits, such as #1C1C1C. |
link | string | required | # and six hex digits, such as #1C1C1C. |
quote | string | required | # and six hex digits, such as #1C1C1C. |
syntax | object | optional (required to publish, with --require-complete) | syntax |
diagram | object | optional (required to publish, with --require-complete) | diagram |
syntax
| Field | Type | Presence | Values and notes |
|---|---|---|---|
keyword | string | required | # and six hex digits, such as #1C1C1C. |
string | string | required | # and six hex digits, such as #1C1C1C. |
comment | string | required | # and six hex digits, such as #1C1C1C. |
number | string | required | # and six hex digits, such as #1C1C1C. |
function | string | required | # and six hex digits, such as #1C1C1C. |
type | string | required | # and six hex digits, such as #1C1C1C. |
diagram
| Field | Type | Presence | Values and notes |
|---|---|---|---|
node | string | required | # and six hex digits, such as #1C1C1C. |
nodeBorder | string | required | # and six hex digits, such as #1C1C1C. |
text | string | required | # and six hex digits, such as #1C1C1C. |
line | string | required | # and six hex digits, such as #1C1C1C. |
secondary | string | required | # and six hex digits, such as #1C1C1C. |
tertiary | string | required | # and six hex digits, such as #1C1C1C. |
note | string | required | # and six hex digits, such as #1C1C1C. |
style
| Field | Type | Presence | Values and notes |
|---|---|---|---|
bodySize | number | optional | range |
lineHeight | number | optional | range |
headingWeight | number | optional | range |
radius | number | optional | range |
maxWidth | number | optional | range |
h1 | object | optional | style.h1 |
h2 | object | optional | style.h2 |
blockquote | object | optional | style.blockquote |
hr | object | optional | style.hr |
table | object | optional | style.table |
listMarker | role | optional | a palette role |
inlineCode | role | optional | a palette role — Only text or a syntax role: keyword, string, comment, number, function or type. |
link | object | optional | style.link |
syntax | object | optional | style.syntax |
style.h1
| Field | Type | Presence | Values and notes |
|---|---|---|---|
size | number | optional | range |
letterSpacing | number | optional | range |
align | enum | optional | left, center |
decoration | object | optional | type |
style.h2
| Field | Type | Presence | Values and notes |
|---|---|---|---|
letterSpacing | number | optional | range |
decoration | object | optional | type |
italic | boolean | optional | true, false |
style.blockquote
| Field | Type | Presence | Values and notes |
|---|---|---|---|
style | object | optional | type |
italic | boolean | optional | true, false |
style.hr
| Field | Type | Presence | Values and notes |
|---|---|---|---|
style | object | optional | type |
style.table
| Field | Type | Presence | Values and notes |
|---|---|---|---|
header | object | optional | type |
verticalRules | boolean | optional | true, false |
rounded | boolean | optional | true, false |
style.link
| Field | Type | Presence | Values and notes |
|---|---|---|---|
underline | boolean | optional | true, false |
style.syntax
| Field | Type | Presence | Values and notes |
|---|---|---|---|
boldKeywords | boolean | optional | true, false |
Options with a type
These style options are objects with a type. Each type takes the fields listed for it and no others.
| Option | type | Field | Presence | Type |
|---|---|---|---|---|
style.h1.decoration | rule | — | — | — |
style.h1.decoration | none | — | — | — |
style.h1.decoration | shortRule | color | required | role |
style.h1.decoration | gradientBar | from | required | role |
style.h1.decoration | gradientBar | to | required | role |
style.h2.decoration | rule | — | — | — |
style.h2.decoration | none | — | — | — |
style.h2.decoration | dot | color | required | role |
style.blockquote.style | bar | width | optional | number range |
style.blockquote.style | panel | — | — | — |
style.hr.style | line | color | optional | role |
style.hr.style | line | thickness | optional | number range |
style.hr.style | shortCentered | color | required | role |
style.hr.style | gradient | colors | required | role[] Two or three roles. |
style.table.header | surface | — | — | — |
style.table.header | accentRule | color | required | role |
style.table.header | filled | background | required | role |
style.table.header | filled | text | required | role |
style.table.header | filled | border | optional | role |
Palette roles
A palette role is one of these names:
background, surface, text, muted, border, heading, accent, link, quote, keyword, string, comment, number, function, type
Numbers
A number is snapped to the nearest multiple of its step, and must then lie between min and max. Otherwise the rule is number.range.
| Field | Min | Max | Step |
|---|---|---|---|
style.bodySize | 14 | 18 | 1 |
style.lineHeight | 1.4 | 1.9 | 0.05 |
style.headingWeight | 400 | 900 | 50 |
style.radius | 0 | 16 | 1 |
style.maxWidth | 600 | 1000 | 1 |
style.h1.size | 1.8 | 2.4 | 0.1 |
style.h1.letterSpacing | -0.03 | 0.03 | 0.005 |
style.h2.letterSpacing | -0.03 | 0.03 | 0.005 |
style.blockquote.style.width | 2 | 4 | 1 |
style.hr.style.thickness | 1 | 4 | 1 |
What else is checked
- Contrast. Each pair of colours the preview draws, such as
textonbackground, must reach 4.5:1 for text and 3:1 for lines and other non-text. The scenarios you list raise some colours to 4.5:1. The message names the pair, its ratio and what it needs. - Strings. A name, summary or author name that is empty, too long, or holds a link, a control character or an invisible or bidirectional format character is refused.
- Rule ids. Each problem carries one, such as
contrast.pair,color.hex,number.range,id.pattern,schema.unknownKeyorpalette.incomplete. Fix the file by what the id and thepathsay.
Hand it over as a link
The Build a theme page opens a theme from a link, so the person can tune it by eye. The link is that page's address, then #theme=, then the file's bytes in base64url without padding. The file can be at most 16,384 bytes. This one line builds the link in zsh and in bash, on macOS and Linux:
printf 'https://marsdawn.southern-light.dev/themes/new/#theme=%s\n' "$(base64 < theme.json | tr -d '\n=' | tr '+/' '-_')"
The theme travels after a #, which a browser does not send to the site, and the site does not send it anywhere. The link does stay in your shell history and in the browser's history, and anyone you paste it to can read the theme.
Submit it
The agent prepares the file and the link. The person submits. Both routes ask for the Developer Certificate of Origin and the Apache-2.0 release, and the person makes that certification: the agent does not tick the box or sign off for them.
- Issue. The person opens the link and presses Submit on the Build a theme page, or opens the issue form and pastes the JSON into Theme. A workflow checks the theme and replies.
- Pull request. Change only
themes/<id>/theme.json, setauthor.githubto the pull request author's login, and sign off every commit withgit commit -s. The rules are in CONTRIBUTING.md.
A submitted theme's author name, name, summary and GitHub login become public permanently, so set author.name to a name the person is happy to publish.
Published themes
To see what published themes look like, browse the theme gallery.