和你的 agent 一起做主題
給製作 MarsDawn 預覽主題的 AI agent 的參考。agent 寫出一個 theme.json,用 marsdawn 命令列工具檢查並渲染,再把連結交給你。你在打造一個主題頁面用眼睛調整,然後投稿。下面的欄位表直接讀自 kit 的原始碼,並有檢查確保兩者一致。
寫出一個 theme.json,反覆執行 marsdawn theme validate theme.json --require-complete --json 直到結束代碼為 0,用 marsdawn theme preview 分別渲染淺色與深色,再把連結交給使用者。主題只有參數,不含任何 CSS。
主題是什麼
- 顏色,淺色與深色各一組:基本色盤,以及選填的語法與圖表顏色。每個顏色都是
#RRGGBB。 - 字型設計:
sans、serif或rounded。 - 一組封閉的樣式選項:數字落在固定範圍內,選擇來自固定清單。樣式選項裡的顏色一律是像
accent這樣的色盤角色,不能是顏色字面值。 - 顯示字串:名稱與摘要,以及選填的作者與授權。
不在下面表格裡的鍵是錯誤,同一個鍵寫兩次也是。檔案最大 16,384 位元組。
檢查與渲染
在放有 theme.json 的資料夾裡執行這些指令。需要 marsdawn 工具:見給 AI agent 的 marsdawn 參考。
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
- 離開代碼:0 表示有效,1 表示無效並列出規則 id,2 表示找不到檔案,64 表示用法錯誤。
preview另外有 4(PNG 已存在,傳入--force或換個檔名)和 5(渲染失敗)。 - 加上
--json時,無效的主題會把問題列在issues下,每一項都有ruleid、path和message。修正後再跑一次。 --require-complete是要發布的主題用的檢查:它還要求兩組色盤都有syntax與diagram顏色。css印出預覽使用的 CSS,preview把範例文件渲染成 PNG。兩者都會先驗證,所以無效的主題什麼都不會渲染。- 交出主題之前,先看過淺色與深色兩張 PNG。
從內建主題開始
複製四個內建主題之一再修改。每一個都是完整且有效的檔案:
/assets/theme-sim/kit/themes/dawn.json: 預設主題,也是下面的範例/assets/theme-sim/kit/themes/classic.json: 較素雅、像文件的色盤/assets/theme-sim/kit/themes/modern.json: 較冷調、較現代的色盤/assets/theme-sim/kit/themes/vivid.json: 較明亮、對比較高的色盤
範例:Dawn
這是 dawn.json,一個位元組都沒改。它唯一的樣式選項是 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 } }
}
}
欄位
下面每個物件都用同樣的方式讀取。required 表示這個鍵必須存在,沒有列出的鍵都是錯誤。
型別:string、integer、number、boolean、map(語言標籤對應文字)、enum(列出的值之一)、role(色盤角色)和 object(連到它的表格)。型別後面的 [] 表示清單。
theme.json
| 欄位 | 型別 | 必填與否 | 值與備註 |
|---|---|---|---|
schemaVersion | integer | required | 固定是 1。 |
id | string | required | 1 到 32 個字元:小寫字母與數字,以單一連字號連接成段,例如 calm-green。 |
version | string | required | MAJOR.MINOR.PATCH,例如 1.0.0。 |
name | map | required | 語言標籤對應文字。en 必填,其他鍵是像 zh-Hant 的語言標籤。每段文字最多 48 個字元,不可含連結或換行。 |
summary | map | required | 和 name 的形式相同,每段文字最多 120 個字元。 |
fontDesign | enum | required | sans, serif, rounded |
scenarios | enum[] | required | agent-review, technical-docs, formal-output, notes-sharing — 一個或兩個,不可重複。它們決定主題必須達到多高的對比。 |
author | object | optional | author |
license | string | optional | SPDX 識別碼,例如 Apache-2.0。投稿的主題以 Apache-2.0 釋出。 |
light | object | required | light, dark |
dark | object | required | light, dark |
style | object | optional | style |
author
| 欄位 | 型別 | 必填與否 | 值與備註 |
|---|---|---|---|
name | string | required | 最多 64 個字元,不可含連結。 |
github | string | optional | GitHub 帳號。投稿時請填投稿者自己的帳號。 |
light, dark
| 欄位 | 型別 | 必填與否 | 值與備註 |
|---|---|---|---|
background | string | required | # 加六個十六進位數字,例如 #1C1C1C。 |
surface | string | required | # 加六個十六進位數字,例如 #1C1C1C。 |
text | string | required | # 加六個十六進位數字,例如 #1C1C1C。 |
muted | string | required | # 加六個十六進位數字,例如 #1C1C1C。 |
border | string | required | # 加六個十六進位數字,例如 #1C1C1C。 |
heading | string | required | # 加六個十六進位數字,例如 #1C1C1C。 |
accent | string | required | # 加六個十六進位數字,例如 #1C1C1C。 |
link | string | required | # 加六個十六進位數字,例如 #1C1C1C。 |
quote | string | required | # 加六個十六進位數字,例如 #1C1C1C。 |
syntax | object | optional (發布時必填,用 --require-complete 檢查) | syntax |
diagram | object | optional (發布時必填,用 --require-complete 檢查) | diagram |
syntax
| 欄位 | 型別 | 必填與否 | 值與備註 |
|---|---|---|---|
keyword | string | required | # 加六個十六進位數字,例如 #1C1C1C。 |
string | string | required | # 加六個十六進位數字,例如 #1C1C1C。 |
comment | string | required | # 加六個十六進位數字,例如 #1C1C1C。 |
number | string | required | # 加六個十六進位數字,例如 #1C1C1C。 |
function | string | required | # 加六個十六進位數字,例如 #1C1C1C。 |
type | string | required | # 加六個十六進位數字,例如 #1C1C1C。 |
diagram
| 欄位 | 型別 | 必填與否 | 值與備註 |
|---|---|---|---|
node | string | required | # 加六個十六進位數字,例如 #1C1C1C。 |
nodeBorder | string | required | # 加六個十六進位數字,例如 #1C1C1C。 |
text | string | required | # 加六個十六進位數字,例如 #1C1C1C。 |
line | string | required | # 加六個十六進位數字,例如 #1C1C1C。 |
secondary | string | required | # 加六個十六進位數字,例如 #1C1C1C。 |
tertiary | string | required | # 加六個十六進位數字,例如 #1C1C1C。 |
note | string | required | # 加六個十六進位數字,例如 #1C1C1C。 |
style
| 欄位 | 型別 | 必填與否 | 值與備註 |
|---|---|---|---|
bodySize | number | optional | 範圍 |
lineHeight | number | optional | 範圍 |
headingWeight | number | optional | 範圍 |
radius | number | optional | 範圍 |
maxWidth | number | optional | 範圍 |
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 | 色盤角色 |
inlineCode | role | optional | 色盤角色 — 只能是 text 或語法角色:keyword、string、comment、number、function 或 type。 |
link | object | optional | style.link |
syntax | object | optional | style.syntax |
style.h1
| 欄位 | 型別 | 必填與否 | 值與備註 |
|---|---|---|---|
size | number | optional | 範圍 |
letterSpacing | number | optional | 範圍 |
align | enum | optional | left, center |
decoration | object | optional | type |
style.h2
| 欄位 | 型別 | 必填與否 | 值與備註 |
|---|---|---|---|
letterSpacing | number | optional | 範圍 |
decoration | object | optional | type |
italic | boolean | optional | true, false |
style.blockquote
| 欄位 | 型別 | 必填與否 | 值與備註 |
|---|---|---|---|
style | object | optional | type |
italic | boolean | optional | true, false |
style.hr
| 欄位 | 型別 | 必填與否 | 值與備註 |
|---|---|---|---|
style | object | optional | type |
style.table
| 欄位 | 型別 | 必填與否 | 值與備註 |
|---|---|---|---|
header | object | optional | type |
verticalRules | boolean | optional | true, false |
rounded | boolean | optional | true, false |
style.link
| 欄位 | 型別 | 必填與否 | 值與備註 |
|---|---|---|---|
underline | boolean | optional | true, false |
style.syntax
| 欄位 | 型別 | 必填與否 | 值與備註 |
|---|---|---|---|
boldKeywords | boolean | optional | true, false |
帶 type 的選項
這些樣式選項是帶有 type 的物件。每個 type 只接受為它列出的欄位,其他都不行。
| 選項 | 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 範圍 |
style.blockquote.style | panel | — | — | — |
style.hr.style | line | color | optional | role |
style.hr.style | line | thickness | optional | number 範圍 |
style.hr.style | shortCentered | color | required | role |
style.hr.style | gradient | colors | required | role[] 兩個或三個角色。 |
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 |
色盤角色
色盤角色是下列名稱之一:
background, surface, text, muted, border, heading, accent, link, quote, keyword, string, comment, number, function, type
數字
數字會先對齊到最接近的間隔倍數,再檢查是否落在最小值與最大值之間,否則規則是 number.range。
| 欄位 | 最小值 | 最大值 | 間隔 |
|---|---|---|---|
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 |
還會檢查什麼
- 對比。預覽畫出的每一組顏色配對,例如
background上的text,文字要達到 4.5:1,線條與其他非文字要達到 3:1。你列出的情境會把部分顏色提高到 4.5:1。訊息會指出是哪一組配對、它的比值和需要的比值。 - 字串。名稱、摘要或作者名稱若是空的、太長,或含有連結、控制字元、不可見或雙向格式字元,都會被拒絕。
- 規則 id。每個問題都帶一個,例如
contrast.pair、color.hex、number.range、id.pattern、schema.unknownKey或palette.incomplete。依 id 與path的說明修正檔案。
用連結交給人
打造一個主題頁面可以從連結開啟主題,讓人用眼睛調整。連結是該頁的網址,接著 #theme=,再接檔案位元組的 base64url(不含補位)。檔案最大 16,384 位元組。下面這一行指令在 macOS 與 Linux 的 zsh 和 bash 都能產生連結:
printf 'https://marsdawn.southern-light.dev/zh-hant/themes/new/#theme=%s\n' "$(base64 < theme.json | tr -d '\n=' | tr '+/' '-_')"
主題跟在 # 後面,瀏覽器不會把它送給網站,網站也不會把它送到任何地方。連結確實會留在你的 shell 歷史與瀏覽器歷史裡,你貼給誰,誰就能讀到這個主題。
投稿
agent 準備檔案與連結,由人來投稿。兩條路都會要求 Developer Certificate of Origin 與 Apache-2.0 授權聲明,這個聲明由人本人做出:agent 不會替他勾選方框或簽署。
- Issue。人開啟連結,在「打造一個主題」頁面按下 Submit,或是開啟 issue 表單,把 JSON 貼進 Theme。接著有工作流程檢查主題並回覆。
- Pull request。只改
themes/<id>/theme.json,把author.github設為 pull request 作者的帳號,每個 commit 都用git commit -s簽署。規則在 CONTRIBUTING.md。
投稿主題的作者名稱、名稱、摘要與 GitHub 帳號會永久公開,所以請把 author.name 設成人願意公開的名字。
已發布的主題
想看已發布的主題長什麼樣子,可以逛逛主題庫。