diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index ae90e05..4982266 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -1,7 +1,7 @@
## Theme Change Checklist
- [ ] I edited source-of-truth inputs under `color-system/*` and/or generator logic under `scripts/*` (not hand-editing generated artifacts only).
-- [ ] I ran `pnpm run sync` and included all generated updates (`themes`, `public/themes`, `extension/themes`, `obsidian/themes`, `obsidian/app-theme`, `src/data/tokens.ts`, `src/styles/theme-vars.css`, `docs/theme-baseline.md`, `docs/color-language-report.md`, `reports/color-language-consistency.json`).
+- [ ] I ran `pnpm run sync` and included all generated updates (`themes`, `public/themes`, `extension/themes`, `obsidian/themes`, `obsidian/app-theme`, `zed/extension`, `src/data/tokens.ts`, `src/styles/theme-vars.css`, `docs/theme-baseline.md`, `docs/color-language-report.md`, `reports/color-language-consistency.json`).
- [ ] I ran `pnpm run audit:generated-origin` and confirmed generated outputs are source-linked (`color-system/` or `scripts/` changed together).
- [ ] I ran `pnpm run audit:all`.
- [ ] I ran `pnpm run build`.
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 3174cc9..8e63b27 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -46,6 +46,17 @@ jobs:
- name: Run audits
run: pnpm run audit:all
+ - name: Validate generated Zed themes against the official schema
+ run: |
+ schema_path="$RUNNER_TEMP/zed-theme-v0.2.0.json"
+ curl --fail --silent --show-error --location \
+ https://zed.dev/schema/themes/v0.2.0.json \
+ --output "$schema_path"
+ pnpm dlx ajv-cli@5 validate \
+ --spec=draft7 \
+ -s "$schema_path" \
+ -d 'zed/extension/themes/*.json'
+
- name: Audit dependencies
run: pnpm run audit:deps
@@ -506,6 +517,40 @@ jobs:
--release
--no-generate
+ publish-zed-mirror:
+ runs-on: ubuntu-latest
+ needs: verify
+ env:
+ ZED_PUBLISH_TOKEN: ${{ secrets.ZED_PUBLISH_TOKEN }}
+ steps:
+ - uses: actions/checkout@v7
+ with:
+ fetch-depth: 0
+
+ - uses: pnpm/action-setup@v6
+
+ - uses: actions/setup-node@v6
+ with:
+ node-version: '22'
+ cache: 'pnpm'
+
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile
+
+ - name: Skip publish (no Zed token)
+ if: ${{ env.ZED_PUBLISH_TOKEN == '' }}
+ run: echo "ZED_PUBLISH_TOKEN is missing. Skipping Zed mirror publish."
+
+ # The Registry consumes a dedicated, minimal public repository as a Git
+ # submodule. HearthTheme remains the source; this job only mirrors the
+ # committed extension.toml, theme JSON, README, and license.
+ - name: Publish to Zed mirror
+ if: ${{ env.ZED_PUBLISH_TOKEN != '' }}
+ run: >
+ node scripts/pack-zed-publish.mjs
+ --repo hearth-code/hearthcode-zed
+ --no-generate
+
release-github:
runs-on: ubuntu-latest
permissions:
diff --git a/AGENTS.md b/AGENTS.md
index 590a8a0..70eddba 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -8,7 +8,7 @@ This repository is source-of-truth driven. Treat generated artifacts as outputs
- Active product: `products/active-product.json`.
- Active scheme: `color-system/active-scheme.json`.
- Primary source inputs live under `color-system/schemes/*` and `color-system/framework/*`.
-- Generated outputs include `themes/*`, `public/themes/*`, `extension/themes/*`, `obsidian/*`, `terminal/{warp,windows-terminal,kitty,alacritty,iterm2}/*`, `src/data/tokens.ts`, `src/styles/theme-vars.css`, `docs/theme-baseline.md`, and selected `reports/*`.
+- Generated outputs include `themes/*`, `public/themes/*`, `extension/themes/*`, `obsidian/*`, `terminal/{warp,windows-terminal,kitty,alacritty,iterm2}/*`, `zed/extension/*`, `src/data/tokens.ts`, `src/styles/theme-vars.css`, `docs/theme-baseline.md`, and selected `reports/*`.
## Safe Change Workflow
@@ -42,3 +42,6 @@ If extension payload changes, keep these aligned:
- top section of `extension/CHANGELOG.md`
Never leave placeholder release notes in a publishable change.
+
+The Zed extension manifest version is generated from `releases/color-language.json`.
+Keep `products/hearthcode/channels.zed` false until the Registry entry is live.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 428f035..09f8738 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -9,7 +9,7 @@ Thanks for helping improve HearthTheme.
- Generators and audits: `scripts/*`
- Version source: `releases/color-language.json`
-Generated outputs (`themes/*`, `public/themes/*`, `extension/themes/*`, `obsidian/*`, `terminal/{warp,windows-terminal,kitty,alacritty,iterm2}/*`, reports/docs snapshots) should be produced by scripts, not hand-maintained alone.
+Generated outputs (`themes/*`, `public/themes/*`, `extension/themes/*`, `obsidian/*`, `terminal/{warp,windows-terminal,kitty,alacritty,iterm2}/*`, `zed/extension/*`, reports/docs snapshots) should be produced by scripts, not hand-maintained alone.
## Local Setup
@@ -23,6 +23,9 @@ Generated outputs (`themes/*`, `public/themes/*`, `extension/themes/*`, `obsidia
1. Edit source-of-truth files (`color-system/*`) and/or generator logic (`scripts/*`).
2. Run `pnpm run sync`.
3. If previews changed, run `pnpm run preview:generate`.
+ If the marketing matrix, layouts, or channel assets changed, run
+ `pnpm run marketing:generate` instead; it synchronizes themes and force-renders
+ every declared size.
4. Run:
- `pnpm run check:sync`
- `pnpm run check:preview`
@@ -31,6 +34,9 @@ Generated outputs (`themes/*`, `public/themes/*`, `extension/themes/*`, `obsidia
- `pnpm run build`
5. Commit source + generated outputs in one change set.
+For Zed changes, also run `pnpm run audit:zed`. Install `zed/extension` as a
+development extension before publishing its generated mirror.
+
## Release Notes Requirement
If your PR changes extension release payload (`extension/themes/*`, `extension/package.json`, `extension/CHANGELOG.md`, `extension/icon.png`):
diff --git a/README.ja.md b/README.ja.md
index afa842d..c181d8a 100644
--- a/README.ja.md
+++ b/README.ja.md
@@ -7,9 +7,9 @@
[](https://marketplace.visualstudio.com/items?itemName=hearth-code.hearth-theme)
[](https://theme.hearthcode.dev)
-HearthCode はコードUI向けのテーマファミリーです。核になる方向は Ember と Moss の 2 つだけで、それぞれに Dark と Light を用意し、VS Code、Open VSX 互換エディタ、5 種類のターミナル形式で使えます。Obsidian 版は現在 Moss のみです。
+**温もりか、構造か。意味は明瞭なまま。** HearthCode は校正されたコード UI 向けテーマファミリーです。Ember は暖かくやわらかく、Moss はドライで構造的。異なる質感でも、読むリズムは同じです。どちらも Dark と Light を VS Code、Open VSX 互換エディタ、Zed、5 種類のターミナル形式で提供し、Obsidian では Moss の Dark と Light を提供します。
-
+
## まずはここから
@@ -18,13 +18,15 @@ HearthCode はコードUI向けのテーマファミリーです。核になる
- `Dark`: 混在照明と長時間コーディング向けの基準点。
- `Light`: 昼光や文書作業が多い日に向くライト版。
+
+
## Moss について
`Moss` は GruvDark テーマファミリーから方向的な着想を受けています。特にチャコールと紙のバランス、そして分離のはっきりした構文レーンを参照していますが、HearthCode 独自のセマンティック設計と校正ルールを通して再構成しており、1:1 の複製ではありません。
## Obsidian
-HearthCode は本格的な Obsidian テーマでもあります。同じカラーランゲージを機能的な Markdown に適用し、種類分けされたコールアウト、取り消し線付きの完了タスク、階層化されたリストマーカー、フラットなコードと引用面、タグのピルを、編集ビューと閲覧ビューで一貫して表示します。
+Obsidian では現在 Moss 方向の Dark と Light のみを提供します。同じカラーランゲージを機能的な Markdown に適用し、種類分けされたコールアウト、取り消し線付きの完了タスク、階層化されたリストマーカー、フラットなコードと引用面、タグのピルを、編集ビューと閲覧ビューで一貫して表示します。
Style Settings プラグインにも対応しています。タイポグラフィ(等幅ノート・コメントの直立表示・可読行幅)、コールアウトの濃さ、そしてコントラスト検証済みのアクセント(Moss / Amber / Slate)を調整できます。いずれも校正済みのパレットには手を加えません。
@@ -32,11 +34,13 @@ Style Settings プラグインにも対応しています。タイポグラフ
## インストール
-1. VS Code Marketplace:
-2. Open VSX 互換エディタ:
-3. VS Code Quick Open: `ext install hearth-code.hearth-theme`
-4. Obsidian: — またはアプリ内の **設定 → 外観 → テーマ → 管理** から **HearthCode** を検索。
-5. ターミナル: [Warp、Windows Terminal、Kitty、Alacritty、iTerm2 用テーマ](./terminal/README.md)。最初は `HearthCode Moss Dark` を推奨します。
+- VS Code: [Marketplace](https://marketplace.visualstudio.com/items?itemName=hearth-code.hearth-theme)、または Quick Open で `ext install hearth-code.hearth-theme`。
+- Open VSX 互換エディタ: [Open VSX](https://open-vsx.org/extension/hearth-code/hearth-theme)。
+- Zed: [Zed Extensions](https://zed.dev/extensions/hearthcode-theme)。
+- Obsidian: [Community Themes](https://community.obsidian.md/themes/hearthcode)、または **設定 → 外観 → テーマ → 管理** から **HearthCode** を検索。
+- ターミナル: [Warp、Windows Terminal、Kitty、Alacritty、iTerm2 用テーマ](./terminal/README.md)。最初は `HearthCode Moss Dark` を推奨します。
+
+
## 公開中のテーマ
@@ -47,6 +51,8 @@ Style Settings プラグインにも対応しています。タイポグラフ
## Theme Forge
+
+
プライマリカラーを変えたいときは、**HearthCode: Open Theme Forge** を実行してパネルを開き、色を選ぶと、テーマ全体——構文**および**エディタのクローム(ステータスバー、サイド / アクティビティ / タイトルバー、各サーフェス)——がダーク / ライトの並列プレビューでリアルタイムに染め直されます。**Apply** は結果を theme-scoped color customizations として書き込み(即時反映、リロード不要)、アクティブな HearthCode スキームのダークとライトの 2 バリアントだけを塗り替え、もう一方のスキームには手を触れません——先に Moss か Ember のバリアントへ切り替えてください。**HearthCode: Reset Theme Forge** は Forge が書き込んだものだけを正確に取り除きます。品質は構築時に担保されます。Forge は公開テーマと同じ品質コントラクトに縛られており、構文レーンはまとめて回転して役割の分離を保ち、彩度は安全な帯域にクランプされ、クロームの色みはコントラスト検証を通してエディタ文字を AA に保ち、機能色(ターミナル・エラー・git・diff)はそれぞれの意味を保ちます。
## イタリックを無効にしたい場合
diff --git a/README.md b/README.md
index afd512a..1d401a4 100644
--- a/README.md
+++ b/README.md
@@ -7,9 +7,9 @@
[](https://marketplace.visualstudio.com/items?itemName=hearth-code.hearth-theme)
[](https://theme.hearthcode.dev)
-HearthCode is a theme family for code interfaces with two design directions: Ember and Moss. Each direction ships Dark and Light for VS Code, Open VSX-compatible editors, and five terminal formats; Moss is also available for Obsidian.
+**Warmth or structure. Meaning stays clear.** HearthCode is a calibrated theme family for code interfaces: Ember brings warm softness, while Moss brings dry structure. Different material, same reading rhythm. Both ship in Dark and Light for VS Code, Open VSX-compatible editors, Zed, and five terminal formats; Obsidian ships Moss in Dark and Light.
-
+
## Start Here
@@ -18,13 +18,15 @@ HearthCode is a theme family for code interfaces with two design directions: Emb
- `Dark`: balanced default for mixed light and long coding sessions.
- `Light`: bright-room and docs-heavy version.
+
+
## Note On Moss
`Moss` takes directional inspiration from the GruvDark theme family, especially its charcoal-and-paper balance and clearer split syntax lanes. It is translated through HearthCode's own semantic system and calibration rules rather than copied one-to-one.
## Obsidian
-HearthCode is a first-class Obsidian theme too — the same color language applied to functional Markdown: typed callouts, task states with a struck-through done state, layered list markers, flat code and quote surfaces, and tag pills, kept consistent across edit and reading views.
+Obsidian carries the Moss direction only, in Dark and Light — the same color language applied to functional Markdown: typed callouts, task states with a struck-through done state, layered list markers, flat code and quote surfaces, and tag pills, kept consistent across edit and reading views.
It also integrates with the Style Settings plugin: tune typography (monospace notes, upright comment italics, readable line length), callout intensity, and a contrast-vetted accent (Moss / Amber / Slate) — all without touching the calibrated palette.
@@ -32,11 +34,13 @@ It also integrates with the Style Settings plugin: tune typography (monospace no
## Install
-1. VS Code Marketplace:
-2. Open VSX-compatible editors:
-3. VS Code Quick Open: `ext install hearth-code.hearth-theme`
-4. Obsidian: — or in-app **Settings → Appearance → Themes → Manage**, then search **HearthCode**.
-5. Terminal: [Warp, Windows Terminal, Kitty, Alacritty, and iTerm2 files](./terminal/README.md). Start with `HearthCode Moss Dark`.
+- VS Code: [Marketplace](https://marketplace.visualstudio.com/items?itemName=hearth-code.hearth-theme) or Quick Open with `ext install hearth-code.hearth-theme`.
+- Open VSX-compatible editors: [Open VSX](https://open-vsx.org/extension/hearth-code/hearth-theme).
+- Zed: [Zed Extensions](https://zed.dev/extensions/hearthcode-theme).
+- Obsidian: [Community Themes](https://community.obsidian.md/themes/hearthcode) — or search **HearthCode** under **Settings → Appearance → Themes → Manage**.
+- Terminals: [Warp, Windows Terminal, Kitty, Alacritty, and iTerm2 files](./terminal/README.md). Start with `HearthCode Moss Dark`.
+
+
## Shipped Themes
@@ -47,6 +51,8 @@ It also integrates with the Style Settings plugin: tune typography (monospace no
## Theme Forge
+
+
Want a different primary color? Run **HearthCode: Open Theme Forge** to open a panel, pick a color, and watch the whole theme — syntax **and** editor chrome (status bar, side/activity/title bars, and surfaces) — recolor live in a side-by-side dark/light preview. **Apply** writes the result as theme-scoped color customizations (live, no reload), painting only your active HearthCode scheme's dark and light variants and leaving the other scheme untouched — switch to a Moss or Ember variant first. **HearthCode: Reset Theme Forge** removes exactly what Forge wrote. Quality holds by construction: Forge is bound by the same quality contract as the shipped themes — the syntax lanes rotate together so role separation holds, saturation is clamped to a safe band, the chrome tint is contrast-checked so editor text stays at AA, and functional colors (terminal, errors, git, diff) keep their meaning.
## Prefer No Italics?
diff --git a/README.zh-CN.md b/README.zh-CN.md
index 26b31cb..806c56c 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -7,9 +7,9 @@
[](https://marketplace.visualstudio.com/items?itemName=hearth-code.hearth-theme)
[](https://theme.hearthcode.dev)
-HearthCode 是一套面向代码界面的主题家族,核心只有两条设计方向:Ember 和 Moss。每条方向都提供 Dark 与 Light 两个版本,覆盖 VS Code、Open VSX 兼容编辑器和五种终端格式;其中 Obsidian 目前只有 Moss。
+**温暖或秩序,语义始终清晰。** HearthCode 是一套经过校准的代码界面主题家族:Ember 更暖、更柔,Moss 更干、更有结构。不同质感,同一阅读节奏。两者均提供 Dark 与 Light,覆盖 VS Code、Open VSX 兼容编辑器、Zed 和五种终端格式;Obsidian 提供 Moss 的 Dark 与 Light。
-
+
## 先这样选
@@ -18,13 +18,15 @@ HearthCode 是一套面向代码界面的主题家族,核心只有两条设计
- `Dark`:适合混合光环境和长时间编码的默认起点。
- `Light`:适合白天、强光和文档偏多的工作流。
+
+
## 关于 Moss
`Moss` 的方向灵感来自 GruvDark 主题家族,主要借鉴了它的炭底纸面平衡和更清楚的分槽语法层次;但它仍然通过 HearthCode 自己的语义系统和校准规则来重新翻译,而不是做一比一复刻。
## Obsidian
-HearthCode 同样是一套完整的 Obsidian 主题——把同一套色彩语言用在功能化的 Markdown 上:分类型的 callout、带删除线的已完成任务、分层的列表标记、扁平的代码与引用面,以及标签药丸,并在编辑视图与阅读视图之间保持一致。
+Obsidian 目前只提供 Moss 方向的 Dark 与 Light——把同一套色彩语言用在功能化的 Markdown 上:分类型的 callout、带删除线的已完成任务、分层的列表标记、扁平的代码与引用面,以及标签药丸,并在编辑视图与阅读视图之间保持一致。
它也接入了 Style Settings 插件:可调排版(等宽笔记、注释正体、可读行宽)、callout 强度,以及一组经对比度校验的强调色(Moss / Amber / Slate)——而这一切都不会改动经过校准的调色板。
@@ -32,11 +34,13 @@ HearthCode 同样是一套完整的 Obsidian 主题——把同一套色彩语
## 安装
-1. VS Code Marketplace:
-2. Open VSX 兼容编辑器:
-3. VS Code 快速安装:`ext install hearth-code.hearth-theme`
-4. Obsidian: —— 或应用内 **设置 → 外观 → 主题 → 管理**,搜索 **HearthCode**。
-5. 终端:[Warp、Windows Terminal、Kitty、Alacritty 与 iTerm2 主题文件](./terminal/README.md)。建议从 `HearthCode Moss Dark` 开始。
+- VS Code:[Marketplace](https://marketplace.visualstudio.com/items?itemName=hearth-code.hearth-theme),或在快速安装中输入 `ext install hearth-code.hearth-theme`。
+- Open VSX 兼容编辑器:[Open VSX](https://open-vsx.org/extension/hearth-code/hearth-theme)。
+- Zed:[Zed Extensions](https://zed.dev/extensions/hearthcode-theme)。
+- Obsidian:[社区主题](https://community.obsidian.md/themes/hearthcode),或在 **设置 → 外观 → 主题 → 管理** 中搜索 **HearthCode**。
+- 终端:[Warp、Windows Terminal、Kitty、Alacritty 与 iTerm2 主题文件](./terminal/README.md)。建议从 `HearthCode Moss Dark` 开始。
+
+
## 当前主题
@@ -47,6 +51,8 @@ HearthCode 同样是一套完整的 Obsidian 主题——把同一套色彩语
## Theme Forge
+
+
想换个主色?运行 **HearthCode: Open Theme Forge** 打开面板,选一个颜色,整个主题——语法**以及**编辑器外壳(状态栏、侧边栏 / 活动栏 / 标题栏,以及各表面)——会在深色 / 浅色并排预览里实时重染。**Apply** 会把结果写成 theme-scoped color customizations(即时生效、无需重载),只影响当前 HearthCode 方案的深色与浅色两个变体,另一个方案保持不动——请先切到 Moss 或 Ember 变体。**HearthCode: Reset Theme Forge** 会精确移除 Forge 写入的内容。质量由构建保证:Forge 受与官方主题同一套质量契约约束——语法通道整体旋转以保持角色分离,饱和度被限制在安全带内,外壳染色经过对比度校验以让编辑器文本维持 AA,功能色(终端、错误、git、diff)保持各自语义。
## 不想要斜体?
diff --git a/color-system/schemes/ember/color-contract.json b/color-system/schemes/ember/color-contract.json
index a5c46b3..03c5a11 100644
--- a/color-system/schemes/ember/color-contract.json
+++ b/color-system/schemes/ember/color-contract.json
@@ -21,11 +21,11 @@
"hueBand": [18, 24],
"minSaturation": 0.24
},
- "livedInSage": {
- "description": "Strings should provide a calm lived-in green reading lane.",
+ "warmWheatLiteral": {
+ "description": "Strings should occupy a baked-wheat literal lane, visibly separate from olive property structure.",
"roles": ["string"],
- "hueBand": [85, 105],
- "minSaturation": 0.14
+ "hueBand": [28, 45],
+ "minSaturation": 0.3
},
"dryOchreType": {
"description": "Types should read as dry ochre classification signals.",
@@ -53,9 +53,10 @@
"method": { "family": "leather", "tone": "base" },
"type": { "family": "ochre", "tone": "base" },
"number": { "family": "terracotta", "tone": "numeric" },
- "string": { "family": "sage", "tone": "literal" }
+ "string": { "family": "wheat", "tone": "literal" }
},
"criticalPairs": [
+ { "left": "property", "right": "string", "minDeltaE": 24 },
{ "left": "keyword", "right": "string", "minDeltaE": 9 },
{ "left": "function", "right": "string", "minDeltaE": 8 },
{ "left": "keyword", "right": "number", "minDeltaE": 8 }
diff --git a/color-system/schemes/ember/foundation.json b/color-system/schemes/ember/foundation.json
index d1f0902..7cb1d13 100644
--- a/color-system/schemes/ember/foundation.json
+++ b/color-system/schemes/ember/foundation.json
@@ -92,15 +92,20 @@
}
},
"sage": {
- "description": "Calm literal band for strings and reading rhythm.",
+ "description": "Calm affirmative green for healthy-state feedback.",
"tones": {
"base": {
"dark": "#98ab7d",
"light": "#6a7d58"
- },
+ }
+ }
+ },
+ "wheat": {
+ "description": "Warm baked-grain literal band that separates strings from olive property structure.",
+ "tones": {
"literal": {
- "dark": "#8eaa79",
- "light": "#6a7f58"
+ "dark": "#c39b69",
+ "light": "#80572d"
}
}
},
diff --git a/color-system/schemes/ember/philosophy.md b/color-system/schemes/ember/philosophy.md
index 7d766ca..3e08ac9 100644
--- a/color-system/schemes/ember/philosophy.md
+++ b/color-system/schemes/ember/philosophy.md
@@ -21,7 +21,7 @@ The palette speaks through a small set of named families:
- `ember`: control-flow pressure and release accents
- `mineral`: a bounded cool anchor for structure
-- `sage`: calm literal rhythm
+- `wheat`: warm literal rhythm, separated from olive property structure
- `ochre`: dry structural classification
- `chalk`: readable carrier neutrals
diff --git a/color-system/schemes/ember/scheme.json b/color-system/schemes/ember/scheme.json
index f620316..2bba74e 100644
--- a/color-system/schemes/ember/scheme.json
+++ b/color-system/schemes/ember/scheme.json
@@ -19,7 +19,7 @@
"vocabulary": [
"ember",
"mineral",
- "sage",
+ "wheat",
"ochre",
"chalk"
],
diff --git a/color-system/schemes/ember/semantic-rules.json b/color-system/schemes/ember/semantic-rules.json
index 82e5576..1b6a51e 100644
--- a/color-system/schemes/ember/semantic-rules.json
+++ b/color-system/schemes/ember/semantic-rules.json
@@ -31,7 +31,7 @@
"flags": { "nearForeground": true, "contrastCritical": true, "allowEscapeHatch": false }
},
"string": {
- "source": { "family": "sage", "tone": "literal" },
+ "source": { "family": "wheat", "tone": "literal" },
"flags": { "nearForeground": false, "contrastCritical": false, "allowEscapeHatch": false }
},
"number": {
diff --git a/color-system/schemes/ember/taxonomy.json b/color-system/schemes/ember/taxonomy.json
index 645c489..1d32c71 100644
--- a/color-system/schemes/ember/taxonomy.json
+++ b/color-system/schemes/ember/taxonomy.json
@@ -15,7 +15,8 @@
"structuralWarmth": [
"brass",
"leather",
- "ochre"
+ "ochre",
+ "wheat"
],
"coolAnchor": [
"mineral"
diff --git a/docs/marketing/README.md b/docs/marketing/README.md
new file mode 100644
index 0000000..4ed6832
--- /dev/null
+++ b/docs/marketing/README.md
@@ -0,0 +1,239 @@
+# HearthCode Marketing System
+
+HearthCode should be presented as a compact, calibrated theme family rather than
+as a growing list of unrelated ports.
+
+## Product truth
+
+- The theme directions are **Ember** and **Moss**.
+- Each direction ships in **Dark** and **Light**.
+- **Amber** is an Obsidian accent preset. It is not a HearthCode theme direction.
+- Theme Forge is a VS Code capability, not a separate theme family or a feature
+ promised on every platform.
+
+| Surface or channel | Ember | Moss | Platform-specific capability |
+| --- | --- | --- | --- |
+| VS Code | Dark + Light | Dark + Light | Theme Forge |
+| Open VSX editors | Dark + Light | Dark + Light | — |
+| Zed | Dark + Light | Dark + Light | — |
+| Terminal packs | Dark + Light | Dark + Light | Five generated formats |
+| Obsidian | — | Dark + Light | Style Settings |
+
+`products/hearthcode/product.json` is the source of truth for supported schemes
+and channels. Marketing assets must derive availability from that manifest rather
+than maintain a second hand-written platform list.
+
+## Positioning
+
+Primary message:
+
+> Warmth or structure. Meaning stays clear.
+
+Supporting message:
+
+> Different material. Same reading rhythm.
+
+`EMBER` and `MOSS` remain the product lockup. Ember brings warm softness; Moss
+brings dry structure. Each direction ships in Dark and Light, with the same
+semantic roles expressed through a different material character. `Four themes.
+One color language.` is system proof, not the lead campaign headline.
+
+Proof should be shown in this order:
+
+1. Two visibly distinct material directions.
+2. Dark and Light designed as pairs.
+3. Accurate platform availability.
+4. Generated and audited semantic consistency.
+5. Platform-specific capabilities such as Theme Forge and Style Settings.
+
+Do not lead with the number of platforms, Theme Forge, or the calibration
+implementation. Those are supporting proof, not the product definition.
+
+## Visual direction: Semantic Materials
+
+The system should make two material atmospheres memorable while keeping real
+code, Markdown, and terminal content crisp. It should not force every channel
+into the same poster shell.
+
+- Use charcoal and paper as the dominant fields.
+- Use Ember and Moss as asymmetric material accents, never as a generic rainbow.
+- Texture belongs to the substrate. Syntax and reading content remain sharp.
+- Keep screenshots and generated UI previews honest. A generated representation
+ must not pretend to be a literal screenshot from an application.
+- Use one strong statement per asset. Installation, customization, and platform
+ coverage belong in separate frames.
+- Reserve the torn-paper rift for family-level attraction assets. Platform proof
+ images inherit color and spacing, not the tear or field-guide chrome.
+
+The visual system has three jobs:
+
+1. **Attraction** — family hero, OG, and social covers use one memorable material
+ boundary and no documentation-density labels.
+2. **Product proof** — editor, Obsidian, terminal, and Forge imagery gives the
+ working surface at least 70% of the canvas.
+3. **System proof** — direction atlases, availability matrices, and calibration
+ diagrams may use the denser Color Field Guide language.
+
+### Material treatment
+
+Family-level assets use a deterministic print layer: sparse paper grain, short
+fibers, restrained display-type wear, and a structural torn-paper rift between
+the wide Ember/Moss fields. The rift uses a jagged field boundary,
+a narrow exposed paper core, a flat dark undercut, Ember-side abrasion,
+projecting fibers, and detached chips rather than a decorative straight line or
+drop shadow. Every layer is generated as SVG primitives; it is not a baked
+raster overlay or an AI-authored palette source.
+
+- Texture ink must be injected from the foreground or surface tokens of the
+ theme underneath it.
+- Most pixels remain the exact shipped surface color so color-fidelity checks
+ can continue to prove the source palette.
+- Proof/code regions use clean theme surfaces without a decorative texture layer.
+- Torn-paper geometry must stay deterministic for the same dimensions and seed;
+ its paper, warm edge, cool flecks, and shadow inks must be injected from real
+ theme tokens.
+- App-like editor and Forge demonstrations remain clean UI evidence rather than
+ pretending to be printed screenshots.
+
+## Content matrix
+
+Every asset has one job and one source-owned sample. Do not substitute decorative
+pseudocode or generic product copy at export time.
+
+| Asset | Message | Sample proof |
+| --- | --- | --- |
+| Family overview / social card | `EMBER` and `MOSS`, separated by their material fields, with `WARMTH OR STRUCTURE. MEANING STAYS CLEAR.` | One valid TypeScript object repeated across all four themes; only `direction` and `mode` change. |
+| VS Code / Open VSX / Zed | `Same roles. Different material.` | The shared TypeScript theme object. |
+| Obsidian | `Color as reading order.` | Markdown heading, callout, and task list. |
+| Terminal packs | `Meaning survives the surface.` | The real `pnpm run verify` command and audit result labels. |
+| Theme Forge | `Your color. Same safeguards.` | Choose direction, pick seed, preview both modes, apply, and restore. |
+
+The copy and samples live in `products/hearthcode/preview.json`. The preview
+generator must consume them directly so the website, repository, and extension
+exports cannot silently diverge.
+
+## Asset compiler
+
+Final marketing images are deterministic build artifacts. AI may be used to
+explore a direction, but it is not a color, copy, logo, or layout source for a
+shipped asset.
+
+The compiler has three explicit layers:
+
+1. `scripts/marketing/brand-system.mjs` owns typography stacks, field-guide
+ measurements, registration marks, and safe structural color operations.
+2. `scripts/marketing/template-components.mjs` owns reusable SVG composition
+ primitives. Templates recompose for each aspect ratio rather than crop a
+ single master image.
+3. `products/hearthcode/marketing-assets.json` owns formats, channels,
+ templates, and output paths. `schemas/marketing-assets.schema.json` guards
+ that source file.
+
+Run `pnpm run marketing:generate` after a theme, product, preview-copy, template,
+or output-spec change. The command synchronizes theme outputs first, then rebuilds
+the full marketing matrix.
+
+### Output matrix
+
+| Layer | Outputs |
+| --- | --- |
+| Family | README wide, GitHub social, site OG, square, portrait, and story/short-video cover. |
+| Direction | Ember and Moss square specimens plus the combined direction atlas. |
+| Editor | VS Code/Open VSX editor proof and the four-theme family overview. |
+| Platform | Zed proof, terminal proof, generated availability matrix, and the separately generated real Obsidian hero. |
+| Capability | Theme Forge workflow. |
+
+The GitHub social export is `1280×640`, matching
+[GitHub's best-display recommendation](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/customizing-your-repositorys-social-media-preview).
+The site OG remains `1200×630`. Social square, feed portrait,
+and story layouts are `1200×1200`, `1080×1350`, and `1080×1920` respectively.
+
+### Aspect-ratio compositions
+
+The family exports share typography, theme-derived colors, and specimen
+components, but they do not share one stretched layout:
+
+| Composition | Formats | Editorial job |
+| --- | --- | --- |
+| `semantic-rift-wide` | README, GitHub social, site OG | A left-to-right Ember/Moss rift with all four themes visible. |
+| `editorial-square` | 1:1 social | A compact Dark/Light matrix with large, readable syntax proof. |
+| `stacked-directions` | 4:5 feed | Ember and Moss become two stacked direction specimens; each pairs Dark and Light. |
+| `campaign-story` | 9:16 story / short-video cover | A poster hierarchy with one large dark proof, one light proof, and no full-height rift. |
+
+The selected composition is declared beside each family asset in
+`products/hearthcode/marketing-assets.json`. Do not infer layout from width or
+height in the renderer.
+
+The wide composition also enforces semantic layout invariants: `EMBER` and its
+code proof remain on the Ember field, while `MOSS` aligns with the Moss proof
+column instead of hugging the torn edge. The material split is the separator, so
+the display lockup does not add a slash glyph. The four code samples are large
+enough to function as product evidence at README scale and remain legible in a
+240px-wide thumbnail. The supporting sentence is split across the two fields so
+the torn boundary never crosses live text.
+
+Platform proofs do not reuse the family poster wrapper. The editor proof is a
+large Moss Dark/Light code comparison with minimal chrome and exact role rails.
+The Obsidian proof contains only Moss and gives one oversized functional Markdown
+frame the full canvas; a diagonal mode cut supplies the Dark/Light contrast with
+only small `DARK` and `LIGHT` labels. Zed and terminal continue to show both
+directions because both actually ship there, but their later template revisions
+should follow the same product-first rule.
+
+## Color fidelity contract
+
+Marketing composition may be expressive, but its palette is not a separate
+design surface.
+
+- Theme surfaces, foregrounds, syntax samples, and palette swatches must use
+ exact values read from `themes/ember-dark.json`, `themes/ember-light.json`,
+ `themes/moss-dark.json`, and `themes/moss-light.json`.
+- A color shown as a Theme Forge seed must be an existing shipped theme token.
+- Alpha and mixes of shipped tokens are allowed only for layout scaffolding such
+ as grids, borders, and muted labels. They must never be presented as theme
+ swatches or syntax colors.
+- `reports/preview-manifest.json` records the source path, SHA-256, and exact
+ colors used for every theme. Generation fails when a required token is absent.
+- Automated tests inspect generated PNG pixels and require every canonical
+ surface and syntax color to be present. Source and manifest values must match
+ exactly; PNG checks allow at most one 8-bit RGB unit for SVG rasterizer
+ rounding. A plausible-looking substitute is therefore a failing build, not an
+ acceptable approximation.
+
+## Canonical assets
+
+All channel-specific exports should derive from a small set of master assets:
+
+- `extension/images/family-overview.png`: Ember/Moss × Dark/Light family overview.
+- `extension/images/editor-moss-dark-light.png`: product-first Moss Dark/Light
+ editor proof with enlarged syntax and exact semantic role rails.
+- `extension/images/theme-forge-workflow.png`: VS Code-only Forge capability.
+- `docs/marketing/direction-atlas.png`: material and syntax distinction between
+ Ember and Moss.
+- `docs/marketing/platform-coverage.png`: accurate availability matrix.
+- `docs/marketing/moss-surfaces.png`: generated semantic examples for code,
+ notes, and terminal. It is a system diagram, not an app screenshot.
+- `docs/marketing/obsidian-hero.png`: full-canvas Moss Dark/Light functional
+ Markdown proof with no family-poster wrapper.
+- `public/og-hearth.png`: current family-level social card.
+- `docs/marketing/exports/github-social.png`: GitHub repository social preview.
+- `docs/marketing/exports/family-{square,portrait,story}.png`: responsive social
+ compositions; these are recomposed layouts, not crops.
+- `docs/marketing/exports/{ember,moss}-square.png`: single-direction campaign
+ cards with real Dark and Light syntax.
+- `zed/images/hearthcode-zed.png`: generated Zed mirror README proof.
+- `terminal/hearthcode-terminal.png`: generated terminal README proof.
+
+The preview generator owns these files. Update product, theme, preview, or
+generator sources and run `pnpm run marketing:generate`; do not edit the PNGs by
+hand.
+
+## Motion and sound
+
+Motion assets should be derived only after the still system is approved.
+
+- A family film shows Ember/Moss and Dark/Light. It does not explain Forge.
+- Forge, Obsidian, and each direction receive separate short demonstrations.
+- Use real theme switching and UI interaction instead of zooming static images.
+- The video must work muted. Prefer silence or restrained interaction sounds over
+ an ambient music bed until HearthCode has a deliberate audio identity.
diff --git a/docs/marketing/direction-atlas.png b/docs/marketing/direction-atlas.png
new file mode 100644
index 0000000..813497a
Binary files /dev/null and b/docs/marketing/direction-atlas.png differ
diff --git a/docs/marketing/exports/ember-square.png b/docs/marketing/exports/ember-square.png
new file mode 100644
index 0000000..ce3704d
Binary files /dev/null and b/docs/marketing/exports/ember-square.png differ
diff --git a/docs/marketing/exports/family-portrait.png b/docs/marketing/exports/family-portrait.png
new file mode 100644
index 0000000..87837f2
Binary files /dev/null and b/docs/marketing/exports/family-portrait.png differ
diff --git a/docs/marketing/exports/family-square.png b/docs/marketing/exports/family-square.png
new file mode 100644
index 0000000..7b14f0d
Binary files /dev/null and b/docs/marketing/exports/family-square.png differ
diff --git a/docs/marketing/exports/family-story.png b/docs/marketing/exports/family-story.png
new file mode 100644
index 0000000..9de85e3
Binary files /dev/null and b/docs/marketing/exports/family-story.png differ
diff --git a/docs/marketing/exports/github-social.png b/docs/marketing/exports/github-social.png
new file mode 100644
index 0000000..634477b
Binary files /dev/null and b/docs/marketing/exports/github-social.png differ
diff --git a/docs/marketing/exports/moss-square.png b/docs/marketing/exports/moss-square.png
new file mode 100644
index 0000000..3115227
Binary files /dev/null and b/docs/marketing/exports/moss-square.png differ
diff --git a/docs/marketing/moss-surfaces.png b/docs/marketing/moss-surfaces.png
new file mode 100644
index 0000000..c92e9bf
Binary files /dev/null and b/docs/marketing/moss-surfaces.png differ
diff --git a/docs/marketing/obsidian-hero.png b/docs/marketing/obsidian-hero.png
index 959a717..7224f3a 100644
Binary files a/docs/marketing/obsidian-hero.png and b/docs/marketing/obsidian-hero.png differ
diff --git a/docs/marketing/platform-coverage.png b/docs/marketing/platform-coverage.png
new file mode 100644
index 0000000..cbd0924
Binary files /dev/null and b/docs/marketing/platform-coverage.png differ
diff --git a/docs/theme-iteration-playbook.md b/docs/theme-iteration-playbook.md
index dc45680..83b903e 100644
--- a/docs/theme-iteration-playbook.md
+++ b/docs/theme-iteration-playbook.md
@@ -116,7 +116,7 @@ It gives the reviewer the source files, generated artifacts, commands, and manua
Useful review files:
- `color-system/schemes//color-contract.json`
-- `public/previews/preview-contrast-v2.png`
+- `public/previews/family-overview.png`
- `obsidian/app-theme/screenshot.png`
- `themes/-dark.json`
- `themes/-light.json`
diff --git a/extension/CHANGELOG.md b/extension/CHANGELOG.md
index 8de0c89..75c934c 100644
--- a/extension/CHANGELOG.md
+++ b/extension/CHANGELOG.md
@@ -1,3 +1,19 @@
+## 3.7.7
+
+- Add a source-driven marketing asset compiler with shared Color Field Guide tokens, reusable SVG composition primitives, and a schema-validated platform/size matrix.
+- Generate dedicated GitHub social, square, portrait, story, Ember, Moss, Zed, and terminal artwork from the shipped theme JSON instead of independent or AI-authored colors.
+- Refresh the multilingual repository READMEs and generated distribution mirrors so each channel presents the same Ember/Moss story with channel-accurate proof.
+
+## 3.7.6
+
+- Move Ember strings from the overlapping sage lane into a dedicated warm-wheat literal lane, so object properties remain olive while string values read as a clearly separate role in both Dark and Light.
+- Add generated property/string hue and DeltaE guards, then refresh the VS Code, Open VSX, Zed, website, and marketing preview artifacts from the same source palette.
+
+## 3.7.5
+
+- Add a generated Zed theme extension with Moss and Ember dark/light variants, schema validation, and source-linked release tooling.
+- Keep Zed theme-picker selected rows visually distinct from hover, including Moss and Ember Light.
+
## 3.7.4
- Fixed Theme Forge getting stuck on **Loading engine…** on some Windows installations by loading its bundled UI, worker, and theme source in parallel through the extension host instead of the webview resource service. Startup now reports four explicit stages, fails with a clear timeout instead of spinning forever, and offers a full retry.
diff --git a/extension/README.md b/extension/README.md
index 3cb2405..af4ba22 100644
--- a/extension/README.md
+++ b/extension/README.md
@@ -1,8 +1,8 @@
# HearthCode
-Low-glare themes for long coding sessions, built to keep syntax structure clear without turning the editor into a light show. Choose warm **Ember** or dry, editorial **Moss** in Dark and Light, then use the built-in **Theme Forge** when you want your own primary color.
+**Warmth or structure. Meaning stays clear.** Choose warm **Ember** or dry, editorial **Moss** in Dark and Light. Both keep the same reading rhythm through syntax and editor chrome; the built-in **Theme Forge** carries that system into your own primary color.
-
+
## Try it or install
@@ -22,7 +22,7 @@ Low-glare themes for long coding sessions, built to keep syntax structure clear
- **Dark:** balanced for mixed light and long coding sessions.
- **Light:** tuned for bright rooms and docs-heavy work.
-
+
The extension includes:
@@ -35,7 +35,7 @@ The extension includes:
Want your own primary color? Run **HearthCode: Open Theme Forge** inside the editor.
-
+
1. **Pick** Moss or Ember and choose a seed color.
2. **Preview** syntax and editor chrome together in Dark and Light.
diff --git a/extension/images/editor-moss-dark-light.png b/extension/images/editor-moss-dark-light.png
new file mode 100644
index 0000000..479b2c1
Binary files /dev/null and b/extension/images/editor-moss-dark-light.png differ
diff --git a/extension/images/family-overview.png b/extension/images/family-overview.png
new file mode 100644
index 0000000..4a6cee6
Binary files /dev/null and b/extension/images/family-overview.png differ
diff --git a/extension/images/preview-contrast-v2.png b/extension/images/preview-contrast-v2.png
deleted file mode 100644
index 24e93f6..0000000
Binary files a/extension/images/preview-contrast-v2.png and /dev/null differ
diff --git a/extension/images/preview-editor-hero.png b/extension/images/preview-editor-hero.png
deleted file mode 100644
index 58b7b2d..0000000
Binary files a/extension/images/preview-editor-hero.png and /dev/null differ
diff --git a/extension/images/preview-forge-workflow.png b/extension/images/preview-forge-workflow.png
deleted file mode 100644
index cfcde94..0000000
Binary files a/extension/images/preview-forge-workflow.png and /dev/null differ
diff --git a/extension/images/theme-forge-workflow.png b/extension/images/theme-forge-workflow.png
new file mode 100644
index 0000000..03c3ab2
Binary files /dev/null and b/extension/images/theme-forge-workflow.png differ
diff --git a/extension/package.json b/extension/package.json
index 897c1d6..a79d8e9 100644
--- a/extension/package.json
+++ b/extension/package.json
@@ -1,8 +1,8 @@
{
"name": "hearth-theme",
"displayName": "HearthCode Theme",
- "description": "Low-glare dark and light themes for long coding sessions, with warm Ember, editorial Moss, and a built-in Theme Forge customizer.",
- "version": "3.7.4",
+ "description": "Warmth or structure, with meaning kept clear across Dark and Light. Includes calibrated Ember and Moss themes plus the built-in Theme Forge.",
+ "version": "3.7.7",
"publisher": "hearth-code",
"engines": {
"vscode": "^1.70.0"
diff --git a/extension/themes/ember-dark.json b/extension/themes/ember-dark.json
index 7335017..9f54eeb 100644
--- a/extension/themes/ember-dark.json
+++ b/extension/themes/ember-dark.json
@@ -228,7 +228,7 @@
"string.regexp"
],
"settings": {
- "foreground": "#8eaa79"
+ "foreground": "#c39b69"
}
},
{
diff --git a/extension/themes/ember-light.json b/extension/themes/ember-light.json
index 8041e34..0f116b6 100644
--- a/extension/themes/ember-light.json
+++ b/extension/themes/ember-light.json
@@ -134,7 +134,7 @@
"punctuation.definition.comment"
],
"settings": {
- "foreground": "#83776e",
+ "foreground": "#83776d",
"fontStyle": "italic"
}
},
@@ -146,7 +146,7 @@
"keyword.control"
],
"settings": {
- "foreground": "#b34732",
+ "foreground": "#b6442f",
"fontStyle": "bold"
}
},
@@ -156,7 +156,7 @@
"keyword.operator.assignment"
],
"settings": {
- "foreground": "#62685c",
+ "foreground": "#64675f",
"fontStyle": ""
}
},
@@ -185,7 +185,7 @@
"meta.method-call.rust entity.name.function.rust"
],
"settings": {
- "foreground": "#a0684b"
+ "foreground": "#a46746"
}
},
{
@@ -206,7 +206,7 @@
"entity.name.function.member.rust"
],
"settings": {
- "foreground": "#a0684b"
+ "foreground": "#a46746"
}
},
{
@@ -217,7 +217,7 @@
"support.type.property-name"
],
"settings": {
- "foreground": "#555d42"
+ "foreground": "#5c6447"
}
},
{
@@ -228,7 +228,7 @@
"string.regexp"
],
"settings": {
- "foreground": "#6a7f58"
+ "foreground": "#7a4f21"
}
},
{
@@ -239,7 +239,7 @@
"constant.language.undefined"
],
"settings": {
- "foreground": "#9a637e"
+ "foreground": "#8d5e75"
}
},
{
@@ -266,7 +266,7 @@
"variable.other.constant"
],
"settings": {
- "foreground": "#49453f"
+ "foreground": "#4a453f"
}
},
{
@@ -310,7 +310,7 @@
"meta.brace"
],
"settings": {
- "foreground": "#847845"
+ "foreground": "#8c804a"
}
},
{
@@ -319,7 +319,7 @@
"support.constant"
],
"settings": {
- "foreground": "#9a637e"
+ "foreground": "#8d5e75"
}
},
{
@@ -387,14 +387,14 @@
],
"semanticHighlighting": true,
"semanticTokenColors": {
- "variable": "#49453f",
+ "variable": "#4a453f",
"variable.readonly": "#914300",
- "property": "#555d42",
+ "property": "#5c6447",
"property.readonly": "#914300",
"function": "#2e6f8f",
- "method": "#a0684b",
+ "method": "#a46746",
"function.defaultLibrary": "#2e6f8f",
- "method.defaultLibrary": "#a0684b",
+ "method.defaultLibrary": "#a46746",
"class": {
"foreground": "#736a18",
"fontStyle": "italic"
@@ -419,10 +419,10 @@
"foreground": "#736a18",
"fontStyle": "italic"
},
- "enumMember": "#9a637e",
+ "enumMember": "#8d5e75",
"namespace": "#705533",
"keyword": {
- "foreground": "#b34732",
+ "foreground": "#b6442f",
"fontStyle": "bold"
},
"parameter": "#5a4431",
diff --git a/obsidian/app-theme/manifest.json b/obsidian/app-theme/manifest.json
index fa22fd2..5291893 100644
--- a/obsidian/app-theme/manifest.json
+++ b/obsidian/app-theme/manifest.json
@@ -1,6 +1,6 @@
{
"name": "HearthCode",
- "version": "3.7.4",
+ "version": "3.7.7",
"minAppVersion": "1.0.0",
"author": "HearthCode",
"authorUrl": "https://theme.hearthcode.dev"
diff --git a/obsidian/app-theme/screenshot.png b/obsidian/app-theme/screenshot.png
index 99d6a40..376993e 100644
Binary files a/obsidian/app-theme/screenshot.png and b/obsidian/app-theme/screenshot.png differ
diff --git a/obsidian/app-theme/versions.json b/obsidian/app-theme/versions.json
index 1b71623..acb64c4 100644
--- a/obsidian/app-theme/versions.json
+++ b/obsidian/app-theme/versions.json
@@ -41,5 +41,8 @@
"3.7.1": "1.0.0",
"3.7.2": "1.0.0",
"3.7.3": "1.0.0",
- "3.7.4": "1.0.0"
+ "3.7.4": "1.0.0",
+ "3.7.5": "1.0.0",
+ "3.7.6": "1.0.0",
+ "3.7.7": "1.0.0"
}
diff --git a/obsidian/mirror-README.md b/obsidian/mirror-README.md
index 53d255e..2e6771b 100644
--- a/obsidian/mirror-README.md
+++ b/obsidian/mirror-README.md
@@ -1,6 +1,6 @@
# HearthCode for Obsidian
-Warm, calm Dark and Light themes for [Obsidian](https://obsidian.md), designed to make structured notes easy to scan without making the page noisy.
+Moss turns color into reading order for [Obsidian](https://obsidian.md). Its calm Dark and Light modes keep structured notes easy to scan without making the page noisy.

diff --git a/package.json b/package.json
index 16990ad..70ba7d0 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,7 @@
"generate:obsidian": "node scripts/generate-obsidian-themes.mjs",
"generate:obsidian:app": "node scripts/generate-obsidian-app-theme.mjs",
"generate:terminal": "node scripts/generate-terminal-themes.mjs",
+ "generate:zed": "node scripts/generate-zed-themes.mjs",
"generate:color-report": "node scripts/generate-color-language-report.mjs",
"generate:contract-checklist": "node scripts/generate-color-language-contract-checklist.mjs",
"generate:contract-review": "node scripts/generate-color-language-contract-review.mjs",
@@ -30,6 +31,7 @@
"test:site": "node scripts/check-built-site.mjs",
"verify": "pnpm run check:sync && pnpm run check:preview && pnpm run test && pnpm run audit:deps && pnpm run audit:all && pnpm run build && pnpm run test:site",
"preview:generate": "node scripts/generate-preview-images.mjs",
+ "marketing:generate": "pnpm run sync && node scripts/generate-preview-images.mjs --force",
"check:sync": "node scripts/check-sync-clean.mjs",
"check:schemes": "node scripts/check-schemes.mjs",
"check:preview": "node scripts/check-preview-clean.mjs",
@@ -49,12 +51,13 @@
"audit:lineage": "node scripts/audit-lineage.mjs",
"audit:parity": "node scripts/audit-parity.mjs",
"audit:obsidian": "node scripts/obsidian-audit.mjs",
+ "audit:zed": "node scripts/zed-audit.mjs",
"audit:copy": "node scripts/content-sync-audit.mjs",
"audit:claims": "node scripts/claims-audit.mjs",
"audit:cjk": "node scripts/cjk-audit.mjs",
"audit:release": "node scripts/release-consistency-audit.mjs",
"audit:forge": "node scripts/audit-forge-quality.mjs",
- "audit:all": "node scripts/audit-source-schema.mjs && node scripts/theme-audit.mjs && node scripts/audit-source-layer.mjs && node scripts/audit-contract-checklist.mjs && node scripts/audit-contract-review.mjs && node scripts/audit-color-contract.mjs && node scripts/audit-ink-contrast.mjs && node scripts/review-moss-visual.mjs --ci && node scripts/audit-moss-release-contract.mjs && node scripts/check-schemes.mjs && node scripts/audit-lineage.mjs && node scripts/audit-parity.mjs && node scripts/audit-compatibility-boundaries.mjs && node scripts/obsidian-audit.mjs && node scripts/content-sync-audit.mjs && node scripts/claims-audit.mjs && node scripts/generated-origin-audit.mjs && node scripts/cjk-audit.mjs && node scripts/release-consistency-audit.mjs && node scripts/audit-forge-quality.mjs && node scripts/check-preview-clean.mjs",
+ "audit:all": "node scripts/audit-source-schema.mjs && node scripts/theme-audit.mjs && node scripts/audit-source-layer.mjs && node scripts/audit-contract-checklist.mjs && node scripts/audit-contract-review.mjs && node scripts/audit-color-contract.mjs && node scripts/audit-ink-contrast.mjs && node scripts/review-moss-visual.mjs --ci && node scripts/audit-moss-release-contract.mjs && node scripts/check-schemes.mjs && node scripts/audit-lineage.mjs && node scripts/audit-parity.mjs && node scripts/audit-compatibility-boundaries.mjs && node scripts/obsidian-audit.mjs && node scripts/zed-audit.mjs && node scripts/content-sync-audit.mjs && node scripts/claims-audit.mjs && node scripts/generated-origin-audit.mjs && node scripts/cjk-audit.mjs && node scripts/release-consistency-audit.mjs && node scripts/audit-forge-quality.mjs && node scripts/check-preview-clean.mjs",
"bump:release:patch": "node scripts/bump-extension-version.mjs patch",
"bump:release:minor": "node scripts/bump-extension-version.mjs minor",
"bump:release:major": "node scripts/bump-extension-version.mjs major",
@@ -75,7 +78,8 @@
"pack:moss:local": "node scripts/pack-local-preview.mjs moss moss-local",
"pack:obsidian": "node scripts/pack-obsidian.mjs",
"pack:obsidian:publish": "node scripts/pack-obsidian-publish.mjs --repo hearth-code/hearthcode-obsidian",
- "pack:obsidian:snippets": "node scripts/pack-obsidian-snippets.mjs"
+ "pack:obsidian:snippets": "node scripts/pack-obsidian-snippets.mjs",
+ "pack:zed:publish": "node scripts/pack-zed-publish.mjs --repo hearth-code/hearthcode-zed"
},
"devDependencies": {
"husky": "^9.1.7",
@@ -85,6 +89,7 @@
"pnpm": {
"overrides": {
"esbuild": "^0.28.1",
+ "postcss": "^8.5.18",
"rollup": "npm:@rollup/wasm-node@4.60.2",
"yaml": "^2.8.3"
}
@@ -92,7 +97,7 @@
"dependencies": {
"@astrojs/sitemap": "^3.7.3",
"@tailwindcss/vite": "^4.3.2",
- "astro": "^7.0.7",
+ "astro": "^7.1.0",
"shiki": "^4.3.1",
"tailwindcss": "^4.3.2"
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 313c7a4..e8dd78e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -6,6 +6,7 @@ settings:
overrides:
esbuild: ^0.28.1
+ postcss: ^8.5.18
rollup: npm:@rollup/wasm-node@4.60.2
yaml: ^2.8.3
@@ -20,8 +21,8 @@ importers:
specifier: ^4.3.2
version: 4.3.2(vite@8.1.4(@types/node@24.12.2)(esbuild@0.28.1)(jiti@2.7.0))
astro:
- specifier: ^7.0.7
- version: 7.0.7(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@rollup/wasm-node@4.60.2)(@types/node@24.12.2)(jiti@2.7.0)
+ specifier: ^7.1.0
+ version: 7.1.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@rollup/wasm-node@4.60.2)(@types/node@24.12.2)(jiti@2.7.0)
shiki:
specifier: ^4.3.1
version: 4.3.1
@@ -109,8 +110,8 @@ packages:
'@astrojs/internal-helpers@0.10.1':
resolution: {integrity: sha512-5phcroT/vmOOrYuuAxtkbPixy5hePtlz9i8K4OeDv3dNK6/UQRuXPOSRTxIOBbUY5Sonw2UaxjbuVc43Mcir6Q==}
- '@astrojs/markdown-satteri@0.3.3':
- resolution: {integrity: sha512-Lje33Ittd8UQGgbIIWQvhPkj5X5c4b1sZnZWX3JQV/AWpfbuQGxVi2ONt6+ScydcwfR4egilslEWyczMclrJ1g==}
+ '@astrojs/markdown-satteri@0.3.4':
+ resolution: {integrity: sha512-6Lvt/bQZEBW+zzdhPblvfZEy5PGEYJaUsUqaCgwHeRPxZJL1gc9I+DRLKWJjjYTWDzVUTzXlMq4WwSK+X34CVw==}
'@astrojs/prism@4.0.2':
resolution: {integrity: sha512-KTivpmnz6lDsC6o9H4+DNm2SrE/GHzw8cNAvEJwAvUT+eoaEnn/4NtbDNfRRaxaJHdp15gf+tfHAWiXR4wB3BA==}
@@ -847,8 +848,8 @@ packages:
resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
engines: {node: '>= 0.4'}
- astro@7.0.7:
- resolution: {integrity: sha512-swqrKDSI/B83GFroYPZYMFcxqbSe9+tkynu1WDBk3GLgBfV9++qVM4Z+2uFT6uu9A53Q0TROnxLketfMEENuqQ==}
+ astro@7.1.3:
+ resolution: {integrity: sha512-4dhPyAAXthf3xLEYnG8SeL7yr/nTPPABfY7e9YF0yuO+vK9Xp+8Q5j4xzsmL3GueukQv4oNwGNTBepLOiDGeJA==}
engines: {node: '>=22.12.0', npm: '>=9.6.5', pnpm: '>=7.1.0'}
hasBin: true
peerDependencies:
@@ -902,9 +903,9 @@ packages:
cookie-es@1.2.3:
resolution: {integrity: sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==}
- cookie@1.1.1:
- resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
- engines: {node: '>=18'}
+ cookie@2.0.1:
+ resolution: {integrity: sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w==}
+ engines: {node: '>=22'}
crossws@0.3.5:
resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==}
@@ -977,6 +978,10 @@ packages:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
+ entities@6.0.1:
+ resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
+ engines: {node: '>=0.12'}
+
es-module-lexer@2.3.0:
resolution: {integrity: sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==}
@@ -1041,12 +1046,24 @@ packages:
h3@1.15.11:
resolution: {integrity: sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==}
+ hast-util-from-html@2.0.3:
+ resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==}
+
+ hast-util-from-parse5@8.0.3:
+ resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==}
+
+ hast-util-parse-selector@4.0.0:
+ resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==}
+
hast-util-to-html@9.0.5:
resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==}
hast-util-whitespace@3.0.0:
resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
+ hastscript@9.0.1:
+ resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==}
+
html-escaper@3.0.3:
resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==}
@@ -1196,13 +1213,13 @@ packages:
resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
engines: {node: '>=10'}
- nanoid@3.3.15:
- resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==}
+ nanoid@3.3.16:
+ resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
- neotraverse@0.6.18:
- resolution: {integrity: sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==}
+ neotraverse@1.0.1:
+ resolution: {integrity: sha512-WmmLty1YWwJl9yZi77v2dVIV6X2kuYV8YYBI/G3LWGKdGHmHUvL1z7FW0iDvEvGAwNEoc5x1tOOOyDnf5jJw/w==}
engines: {node: '>= 10'}
nlcst-to-string@4.0.0:
@@ -1252,6 +1269,9 @@ packages:
package-manager-detector@1.7.0:
resolution: {integrity: sha512-xg1eHpwYL/D/HEdWw2goFZP6vV0FH7W+PZ5rFkGjdIDLtxq7EkzBUeT3m+lndYCt8wKbmofUu1MUdMCXkCk9ZQ==}
+ parse5@7.3.0:
+ resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
+
piccolore@0.1.3:
resolution: {integrity: sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==}
@@ -1266,8 +1286,8 @@ packages:
resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==}
engines: {node: '>=12'}
- postcss@8.5.17:
- resolution: {integrity: sha512-J7EF+8X+CzRPaJPOv9Ck2wNWJvGnnl3PcNPAdGg6GTLjyVpyQ0yATMSXRFRV01BviT/9Gwuc3rjEyJbDJG9a4w==}
+ postcss@8.5.23:
+ resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==}
engines: {node: ^10 || ^12 || >=14}
prismjs@1.30.0:
@@ -1489,6 +1509,9 @@ packages:
uploadthing:
optional: true
+ vfile-location@5.0.3:
+ resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==}
+
vfile-message@4.0.3:
resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==}
@@ -1546,6 +1569,9 @@ packages:
vite:
optional: true
+ web-namespaces@2.0.1:
+ resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==}
+
xxhash-wasm@1.1.0:
resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==}
@@ -1633,11 +1659,12 @@ snapshots:
smol-toml: 1.7.0
unified: 11.0.5
- '@astrojs/markdown-satteri@0.3.3':
+ '@astrojs/markdown-satteri@0.3.4':
dependencies:
'@astrojs/internal-helpers': 0.10.1
'@astrojs/prism': 4.0.2
github-slugger: 2.0.0
+ hast-util-from-html: 2.0.3
satteri: 0.9.5
'@astrojs/prism@4.0.2':
@@ -2184,11 +2211,11 @@ snapshots:
aria-query@5.3.2: {}
- astro@7.0.7(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@rollup/wasm-node@4.60.2)(@types/node@24.12.2)(jiti@2.7.0):
+ astro@7.1.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@rollup/wasm-node@4.60.2)(@types/node@24.12.2)(jiti@2.7.0):
dependencies:
'@astrojs/compiler-rs': 0.3.1(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)
'@astrojs/internal-helpers': 0.10.1
- '@astrojs/markdown-satteri': 0.3.3
+ '@astrojs/markdown-satteri': 0.3.4
'@astrojs/telemetry': 3.3.3
'@capsizecss/unpack': 4.0.1
'@clack/prompts': 1.7.0
@@ -2200,7 +2227,7 @@ snapshots:
ci-info: 4.4.0
clsx: 2.1.1
common-ancestor-path: 2.0.0
- cookie: 1.1.1
+ cookie: 2.0.1
devalue: 5.8.1
diff: 8.0.4
dset: 3.1.4
@@ -2217,7 +2244,7 @@ snapshots:
magic-string: 0.30.21
magicast: 0.5.3
mrmime: 2.0.1
- neotraverse: 0.6.18
+ neotraverse: 1.0.1
obug: 2.1.3
p-limit: 7.3.0
p-queue: 9.3.1
@@ -2304,7 +2331,7 @@ snapshots:
cookie-es@1.2.3: {}
- cookie@1.1.1: {}
+ cookie@2.0.1: {}
crossws@0.3.5:
dependencies:
@@ -2377,6 +2404,8 @@ snapshots:
entities@4.5.0: {}
+ entities@6.0.1: {}
+
es-module-lexer@2.3.0: {}
esbuild@0.28.1:
@@ -2461,6 +2490,30 @@ snapshots:
ufo: 1.6.4
uncrypto: 0.1.3
+ hast-util-from-html@2.0.3:
+ dependencies:
+ '@types/hast': 3.0.5
+ devlop: 1.1.0
+ hast-util-from-parse5: 8.0.3
+ parse5: 7.3.0
+ vfile: 6.0.3
+ vfile-message: 4.0.3
+
+ hast-util-from-parse5@8.0.3:
+ dependencies:
+ '@types/hast': 3.0.5
+ '@types/unist': 3.0.3
+ devlop: 1.1.0
+ hastscript: 9.0.1
+ property-information: 7.2.0
+ vfile: 6.0.3
+ vfile-location: 5.0.3
+ web-namespaces: 2.0.1
+
+ hast-util-parse-selector@4.0.0:
+ dependencies:
+ '@types/hast': 3.0.5
+
hast-util-to-html@9.0.5:
dependencies:
'@types/hast': 3.0.5
@@ -2479,6 +2532,14 @@ snapshots:
dependencies:
'@types/hast': 3.0.5
+ hastscript@9.0.1:
+ dependencies:
+ '@types/hast': 3.0.5
+ comma-separated-tokens: 2.0.3
+ hast-util-parse-selector: 4.0.0
+ property-information: 7.2.0
+ space-separated-tokens: 2.0.2
+
html-escaper@3.0.3: {}
html-void-elements@3.0.0: {}
@@ -2597,9 +2658,9 @@ snapshots:
mrmime@2.0.1: {}
- nanoid@3.3.15: {}
+ nanoid@3.3.16: {}
- neotraverse@0.6.18: {}
+ neotraverse@1.0.1: {}
nlcst-to-string@4.0.0:
dependencies:
@@ -2646,6 +2707,10 @@ snapshots:
package-manager-detector@1.7.0: {}
+ parse5@7.3.0:
+ dependencies:
+ entities: 6.0.1
+
piccolore@0.1.3: {}
picocolors@1.1.1: {}
@@ -2654,9 +2719,9 @@ snapshots:
picomatch@4.0.5: {}
- postcss@8.5.17:
+ postcss@8.5.23:
dependencies:
- nanoid: 3.3.15
+ nanoid: 3.3.16
picocolors: 1.1.1
source-map-js: 1.2.1
@@ -2886,6 +2951,11 @@ snapshots:
ofetch: 1.5.1
ufo: 1.6.4
+ vfile-location@5.0.3:
+ dependencies:
+ '@types/unist': 3.0.3
+ vfile: 6.0.3
+
vfile-message@4.0.3:
dependencies:
'@types/unist': 3.0.3
@@ -2900,7 +2970,7 @@ snapshots:
dependencies:
lightningcss: 1.32.0
picomatch: 4.0.5
- postcss: 8.5.17
+ postcss: 8.5.23
rolldown: 1.1.5
tinyglobby: 0.2.17
optionalDependencies:
@@ -2913,6 +2983,8 @@ snapshots:
optionalDependencies:
vite: 8.1.4(@types/node@24.12.2)(esbuild@0.28.1)(jiti@2.7.0)
+ web-namespaces@2.0.1: {}
+
xxhash-wasm@1.1.0: {}
yargs-parser@22.0.0: {}
diff --git a/products/hearthcode-moss/release.json b/products/hearthcode-moss/release.json
index 6229a30..0bc764e 100644
--- a/products/hearthcode-moss/release.json
+++ b/products/hearthcode-moss/release.json
@@ -45,7 +45,7 @@
"author": "HearthCode",
"authorUrl": "https://theme.hearthcode.dev",
"minAppVersion": "1.0.0",
- "screenshotSourcePath": "public/previews/preview-contrast-v2.png",
+ "screenshotSourcePath": "public/previews/family-overview.png",
"screenshotPath": "obsidian/app-theme/screenshot.png",
"packageBasename": "hearthcode-moss-obsidian-app-theme",
"modes": [
diff --git a/products/hearthcode/marketing-assets.json b/products/hearthcode/marketing-assets.json
new file mode 100644
index 0000000..6e51762
--- /dev/null
+++ b/products/hearthcode/marketing-assets.json
@@ -0,0 +1,172 @@
+{
+ "schemaVersion": 1,
+ "brandSystem": "semantic-materials-v1",
+ "renderer": "semantic-materials-v1",
+ "formats": {
+ "readme-wide": {
+ "width": 1600,
+ "height": 900,
+ "safeInset": 56
+ },
+ "github-social": {
+ "width": 1280,
+ "height": 640,
+ "safeInset": 48
+ },
+ "site-og": {
+ "width": 1200,
+ "height": 630,
+ "safeInset": 44
+ },
+ "social-square": {
+ "width": 1200,
+ "height": 1200,
+ "safeInset": 64
+ },
+ "social-portrait": {
+ "width": 1080,
+ "height": 1350,
+ "safeInset": 64
+ },
+ "social-story": {
+ "width": 1080,
+ "height": 1920,
+ "safeInset": 72
+ }
+ },
+ "assets": [
+ {
+ "id": "family-readme",
+ "template": "family",
+ "composition": "semantic-rift-wide",
+ "format": "readme-wide",
+ "channels": ["github-readme", "vscode-marketplace", "open-vsx", "website"],
+ "outputs": [
+ "extension/images/family-overview.png",
+ "public/previews/family-overview.png"
+ ]
+ },
+ {
+ "id": "editor-marketplace",
+ "template": "editor-proof",
+ "format": "readme-wide",
+ "channels": ["vscode-marketplace", "open-vsx"],
+ "outputs": ["extension/images/editor-moss-dark-light.png"]
+ },
+ {
+ "id": "forge-marketplace",
+ "template": "forge-workflow",
+ "format": "readme-wide",
+ "channels": ["vscode-marketplace"],
+ "outputs": ["extension/images/theme-forge-workflow.png"]
+ },
+ {
+ "id": "direction-atlas",
+ "template": "direction-atlas",
+ "format": "readme-wide",
+ "channels": ["github-readme", "website", "press-kit"],
+ "outputs": ["docs/marketing/direction-atlas.png"]
+ },
+ {
+ "id": "platform-coverage",
+ "template": "platform-coverage",
+ "format": "readme-wide",
+ "channels": ["github-readme", "website", "press-kit"],
+ "outputs": ["docs/marketing/platform-coverage.png"]
+ },
+ {
+ "id": "moss-surfaces",
+ "template": "moss-surfaces",
+ "format": "readme-wide",
+ "channels": ["github-readme", "website", "press-kit"],
+ "outputs": ["docs/marketing/moss-surfaces.png"]
+ },
+ {
+ "id": "github-social",
+ "template": "family",
+ "composition": "semantic-rift-wide",
+ "format": "github-social",
+ "channels": ["github-social"],
+ "outputs": ["docs/marketing/exports/github-social.png"]
+ },
+ {
+ "id": "site-og",
+ "template": "family",
+ "composition": "semantic-rift-wide",
+ "format": "site-og",
+ "channels": ["website-og"],
+ "outputs": ["public/og-hearth.png"]
+ },
+ {
+ "id": "family-square",
+ "template": "family",
+ "composition": "editorial-square",
+ "format": "social-square",
+ "channels": ["social-square"],
+ "outputs": ["docs/marketing/exports/family-square.png"]
+ },
+ {
+ "id": "family-portrait",
+ "template": "family",
+ "composition": "stacked-directions",
+ "format": "social-portrait",
+ "channels": ["social-feed"],
+ "outputs": ["docs/marketing/exports/family-portrait.png"]
+ },
+ {
+ "id": "family-story",
+ "template": "family",
+ "composition": "campaign-story",
+ "format": "social-story",
+ "channels": ["social-story", "short-video-cover"],
+ "outputs": ["docs/marketing/exports/family-story.png"]
+ },
+ {
+ "id": "ember-square",
+ "template": "direction-card",
+ "format": "social-square",
+ "schemeId": "ember",
+ "channels": ["social-square", "press-kit"],
+ "outputs": ["docs/marketing/exports/ember-square.png"]
+ },
+ {
+ "id": "moss-square",
+ "template": "direction-card",
+ "format": "social-square",
+ "schemeId": "moss",
+ "channels": ["social-square", "press-kit"],
+ "outputs": ["docs/marketing/exports/moss-square.png"]
+ },
+ {
+ "id": "zed-platform",
+ "template": "channel-proof",
+ "format": "readme-wide",
+ "channelId": "zed",
+ "channels": ["zed", "zed-mirror-readme"],
+ "outputs": ["zed/images/hearthcode-zed.png"]
+ },
+ {
+ "id": "terminal-platform",
+ "template": "channel-proof",
+ "format": "readme-wide",
+ "channelId": "terminal",
+ "channels": ["terminal-readme", "press-kit"],
+ "outputs": ["terminal/hearthcode-terminal.png"]
+ }
+ ],
+ "managedAssets": [
+ {
+ "id": "obsidian-hero",
+ "generator": "scripts/generate-obsidian-app-theme.mjs",
+ "format": "readme-wide",
+ "channels": ["github-readme", "obsidian-mirror-readme"],
+ "outputs": ["docs/marketing/obsidian-hero.png"]
+ },
+ {
+ "id": "obsidian-community-screenshot",
+ "generator": "scripts/generate-obsidian-app-theme.mjs",
+ "channels": ["obsidian-community-themes"],
+ "outputs": ["obsidian/app-theme/screenshot.png"]
+ }
+ ]
+}
diff --git a/products/hearthcode/preview.json b/products/hearthcode/preview.json
index d779971..3aa14a9 100644
--- a/products/hearthcode/preview.json
+++ b/products/hearthcode/preview.json
@@ -1,8 +1,8 @@
{
"schemaVersion": 1,
"badgeLabel": "HEARTHCODE",
- "headline": "Four stable starting points",
- "subheadline": "Moss and Ember, each shown in Dark and Light with real syntax and editor chrome.",
+ "headline": "Two directions. Four calibrated themes.",
+ "subheadline": "Ember brings warm softness. Moss brings dry structure. Both ship in Dark and Light.",
"familyLabels": {
"dark": "DARK STARTING POINTS",
"light": "LIGHT STARTING POINTS"
@@ -10,5 +10,99 @@
"variantNames": {
"dark": "Ember Dark",
"light": "Ember Light"
+ },
+ "marketing": {
+ "familyHeadline": "EMBER / MOSS",
+ "familySubheadline": "WARMTH OR STRUCTURE. MEANING STAYS CLEAR.",
+ "directionHeadline": "Warmth or structure. Meaning stays clear.",
+ "directionSubheadline": "Different material. Same reading rhythm.",
+ "platformHeadline": "Where each direction ships.",
+ "platformSubheadline": "Every channel shows only the directions and modes it actually ships.",
+ "mossSurfaceHeadline": "Same roles. Different surface.",
+ "mossSurfaceSubheadline": "Code, notes, and terminal output keep the same visual hierarchy.",
+ "editorsHeadline": "Same roles. Different material.",
+ "editorsSubheadline": "VS Code, Open VSX editors, and Zed share one syntax proof.",
+ "obsidianHeadline": "Color as reading order.",
+ "obsidianSubheadline": "Moss carries structure through Markdown without turning notes into code.",
+ "terminalHeadline": "Meaning survives the surface.",
+ "terminalSubheadline": "Five terminal formats use the shipped ANSI palette.",
+ "forgeHeadline": "Your color. Same safeguards.",
+ "forgeSubheadline": "Choose, preview, apply, and restore inside VS Code.",
+ "directions": {
+ "ember": {
+ "summary": "Warm charcoal and paper, with ember control flow and cool callable anchors.",
+ "chips": ["warm neutrals", "ember control", "denim callables"],
+ "comment": "// warm-neutral structure with cool anchors",
+ "sampleFunction": "renderTheme",
+ "sampleVariable": "theme",
+ "sampleString": "\"ember\"",
+ "sampleValue": "\"hearth\"",
+ "directionLabel": "WARM-NEUTRAL DIRECTION",
+ "focusLabel": "COOL CALLABLES"
+ },
+ "moss": {
+ "summary": "Dry charcoal and paper, with clearer lane split and greener callable structure.",
+ "chips": ["dry paper", "editorial lane split", "lichen callables"],
+ "comment": "// dry editorial lanes with calm callables",
+ "sampleFunction": "routeSignal",
+ "sampleVariable": "palette",
+ "sampleString": "\"moss\"",
+ "sampleValue": "\"field\"",
+ "directionLabel": "DRY EDITORIAL DIRECTION",
+ "focusLabel": "GREEN CALLABLES"
+ }
+ }
+ },
+ "samples": {
+ "family": {
+ "language": "typescript",
+ "lines": [
+ "const theme = {",
+ " direction: \"{direction}\",",
+ " mode: \"{mode}\",",
+ "} as const;"
+ ]
+ },
+ "editors": {
+ "language": "typescript",
+ "lines": [
+ "type ThemePreview = {",
+ " direction: \"ember\" | \"moss\";",
+ " mode: \"dark\" | \"light\";",
+ "};",
+ "const theme: ThemePreview = {",
+ " direction: \"moss\",",
+ " mode: \"{mode}\",",
+ "};"
+ ]
+ },
+ "obsidian": {
+ "language": "markdown",
+ "lines": [
+ "# Color as reading order",
+ "> [!tip] Structure leads. Noise recedes.",
+ "- [x] Dark and Light stay paired",
+ "- [ ] Tune with Style Settings"
+ ]
+ },
+ "terminal": {
+ "language": "shell",
+ "lines": [
+ "$ pnpm run verify",
+ "[PASS] Color contract audit passed.",
+ "[PASS] Zed theme audit passed.",
+ "[PASS] Obsidian theme audit passed."
+ ]
+ },
+ "forge": {
+ "language": "workflow",
+ "lines": [
+ "Choose direction",
+ "Pick seed",
+ "Preview Dark + Light",
+ "Apply",
+ "Restore original theme"
+ ]
+ }
}
}
diff --git a/products/hearthcode/product.json b/products/hearthcode/product.json
index 2834e10..fce315a 100644
--- a/products/hearthcode/product.json
+++ b/products/hearthcode/product.json
@@ -3,12 +3,12 @@
"id": "hearthcode",
"name": "HearthCode",
"displayName": "HearthCode Theme",
- "summary": "A low-glare theme family for code interfaces.",
+ "summary": "A calibrated Ember and Moss theme family for code interfaces.",
"brand": {
"id": "hearthcode-theme",
"name": "HearthCode Theme",
"displayName": "HearthCode Theme",
- "summary": "A theme family for code interfaces."
+ "summary": "Warmth or structure, with one semantic color system."
},
"publisher": "HearthCode",
"websiteUrl": "https://theme.hearthcode.dev",
@@ -77,6 +77,34 @@
"vscode": true,
"openvsx": true,
"obsidian": true,
- "terminal": true
+ "terminal": true,
+ "zed": true
+ },
+ "channelAvailability": {
+ "vscode": {
+ "schemeIds": ["ember", "moss"],
+ "variantIds": ["dark", "light"],
+ "capabilityIds": ["theme-forge"]
+ },
+ "openvsx": {
+ "schemeIds": ["ember", "moss"],
+ "variantIds": ["dark", "light"],
+ "capabilityIds": []
+ },
+ "zed": {
+ "schemeIds": ["ember", "moss"],
+ "variantIds": ["dark", "light"],
+ "capabilityIds": []
+ },
+ "terminal": {
+ "schemeIds": ["ember", "moss"],
+ "variantIds": ["dark", "light"],
+ "capabilityIds": ["five-formats"]
+ },
+ "obsidian": {
+ "schemeIds": ["moss"],
+ "variantIds": ["dark", "light"],
+ "capabilityIds": ["style-settings"]
+ }
}
}
diff --git a/products/hearthcode/release.json b/products/hearthcode/release.json
index ff2c23b..7fa4b3f 100644
--- a/products/hearthcode/release.json
+++ b/products/hearthcode/release.json
@@ -1,13 +1,13 @@
{
"schemaVersion": 1,
"site": {
- "titleDescriptor": "Color Language for Code Interfaces",
+ "titleDescriptor": "Ember and Moss Themes for Code Interfaces",
"ogImagePath": "/og-hearth.png"
},
"vscodeExtension": {
"name": "hearth-theme",
"publisher": "hearth-code",
- "description": "Low-glare dark and light themes for long coding sessions, with warm Ember, editorial Moss, and a built-in Theme Forge customizer.",
+ "description": "Warmth or structure, with meaning kept clear across Dark and Light. Includes calibrated Ember and Moss themes plus the built-in Theme Forge.",
"categories": [
"Themes"
],
@@ -59,12 +59,21 @@
"author": "HearthCode",
"authorUrl": "https://theme.hearthcode.dev",
"minAppVersion": "1.0.0",
- "screenshotSourcePath": "public/previews/preview-contrast-v2.png",
+ "screenshotSourcePath": "public/previews/family-overview.png",
"screenshotPath": "obsidian/app-theme/screenshot.png",
"packageBasename": "hearth-obsidian-app-theme",
"modes": [
"dark",
"light"
]
+ },
+ "zedExtension": {
+ "id": "hearthcode-theme",
+ "name": "HearthCode Theme",
+ "authors": [
+ "HearthCode"
+ ],
+ "description": "Warm Ember or structured Moss in calibrated Dark and Light themes for Zed, with one consistent semantic color system.",
+ "repository": "https://github.com/hearth-code/hearthcode-zed"
}
}
diff --git a/products/moss-local/release.json b/products/moss-local/release.json
index 0e00c48..5594038 100644
--- a/products/moss-local/release.json
+++ b/products/moss-local/release.json
@@ -38,7 +38,7 @@
"author": "HearthCode",
"authorUrl": "https://theme.hearthcode.dev",
"minAppVersion": "1.0.0",
- "screenshotSourcePath": "public/previews/preview-contrast-v2.png",
+ "screenshotSourcePath": "public/previews/family-overview.png",
"screenshotPath": "obsidian/app-theme/screenshot.png",
"packageBasename": "moss-obsidian-app-theme-local",
"modes": [
diff --git a/public/og-hearth.png b/public/og-hearth.png
index 7971d7f..fb975c1 100644
Binary files a/public/og-hearth.png and b/public/og-hearth.png differ
diff --git a/public/previews/family-overview.png b/public/previews/family-overview.png
new file mode 100644
index 0000000..4a6cee6
Binary files /dev/null and b/public/previews/family-overview.png differ
diff --git a/public/previews/preview-contrast-v2.png b/public/previews/preview-contrast-v2.png
deleted file mode 100644
index 24e93f6..0000000
Binary files a/public/previews/preview-contrast-v2.png and /dev/null differ
diff --git a/public/themes/ember-dark.json b/public/themes/ember-dark.json
index 7335017..9f54eeb 100644
--- a/public/themes/ember-dark.json
+++ b/public/themes/ember-dark.json
@@ -228,7 +228,7 @@
"string.regexp"
],
"settings": {
- "foreground": "#8eaa79"
+ "foreground": "#c39b69"
}
},
{
diff --git a/public/themes/ember-light.json b/public/themes/ember-light.json
index 8041e34..0f116b6 100644
--- a/public/themes/ember-light.json
+++ b/public/themes/ember-light.json
@@ -134,7 +134,7 @@
"punctuation.definition.comment"
],
"settings": {
- "foreground": "#83776e",
+ "foreground": "#83776d",
"fontStyle": "italic"
}
},
@@ -146,7 +146,7 @@
"keyword.control"
],
"settings": {
- "foreground": "#b34732",
+ "foreground": "#b6442f",
"fontStyle": "bold"
}
},
@@ -156,7 +156,7 @@
"keyword.operator.assignment"
],
"settings": {
- "foreground": "#62685c",
+ "foreground": "#64675f",
"fontStyle": ""
}
},
@@ -185,7 +185,7 @@
"meta.method-call.rust entity.name.function.rust"
],
"settings": {
- "foreground": "#a0684b"
+ "foreground": "#a46746"
}
},
{
@@ -206,7 +206,7 @@
"entity.name.function.member.rust"
],
"settings": {
- "foreground": "#a0684b"
+ "foreground": "#a46746"
}
},
{
@@ -217,7 +217,7 @@
"support.type.property-name"
],
"settings": {
- "foreground": "#555d42"
+ "foreground": "#5c6447"
}
},
{
@@ -228,7 +228,7 @@
"string.regexp"
],
"settings": {
- "foreground": "#6a7f58"
+ "foreground": "#7a4f21"
}
},
{
@@ -239,7 +239,7 @@
"constant.language.undefined"
],
"settings": {
- "foreground": "#9a637e"
+ "foreground": "#8d5e75"
}
},
{
@@ -266,7 +266,7 @@
"variable.other.constant"
],
"settings": {
- "foreground": "#49453f"
+ "foreground": "#4a453f"
}
},
{
@@ -310,7 +310,7 @@
"meta.brace"
],
"settings": {
- "foreground": "#847845"
+ "foreground": "#8c804a"
}
},
{
@@ -319,7 +319,7 @@
"support.constant"
],
"settings": {
- "foreground": "#9a637e"
+ "foreground": "#8d5e75"
}
},
{
@@ -387,14 +387,14 @@
],
"semanticHighlighting": true,
"semanticTokenColors": {
- "variable": "#49453f",
+ "variable": "#4a453f",
"variable.readonly": "#914300",
- "property": "#555d42",
+ "property": "#5c6447",
"property.readonly": "#914300",
"function": "#2e6f8f",
- "method": "#a0684b",
+ "method": "#a46746",
"function.defaultLibrary": "#2e6f8f",
- "method.defaultLibrary": "#a0684b",
+ "method.defaultLibrary": "#a46746",
"class": {
"foreground": "#736a18",
"fontStyle": "italic"
@@ -419,10 +419,10 @@
"foreground": "#736a18",
"fontStyle": "italic"
},
- "enumMember": "#9a637e",
+ "enumMember": "#8d5e75",
"namespace": "#705533",
"keyword": {
- "foreground": "#b34732",
+ "foreground": "#b6442f",
"fontStyle": "bold"
},
"parameter": "#5a4431",
diff --git a/releases/color-language.json b/releases/color-language.json
index ec729a2..9d04e49 100644
--- a/releases/color-language.json
+++ b/releases/color-language.json
@@ -1,4 +1,4 @@
{
- "version": "3.7.4",
+ "version": "3.7.7",
"channel": "stable"
}
diff --git a/reports/color-contract-audit.json b/reports/color-contract-audit.json
index be74d78..0eebb7c 100644
--- a/reports/color-contract-audit.json
+++ b/reports/color-contract-audit.json
@@ -472,22 +472,22 @@
}
}
},
- "livedInSage": {
+ "warmWheatLiteral": {
"hueBand": [
- 85,
- 105
+ 28,
+ 45
],
- "minSaturation": 0.14,
+ "minSaturation": 0.3,
"minContrast": null,
"minBackgroundHueDistance": null,
"roles": {
"string": {
- "color": "#8eaa79",
- "hue": 94.3,
- "saturation": 0.224,
- "lightness": 0.571,
- "contrast": 6.71,
- "backgroundHueDistance": 71.8
+ "color": "#c39b69",
+ "hue": 33.3,
+ "saturation": 0.429,
+ "lightness": 0.588,
+ "contrast": 6.74,
+ "backgroundHueDistance": 10.8
}
}
},
@@ -531,17 +531,23 @@
}
},
"criticalPairs": [
+ {
+ "left": "property",
+ "right": "string",
+ "minDeltaE": 24,
+ "deltaE": 26
+ },
{
"left": "keyword",
"right": "string",
"minDeltaE": 9,
- "deltaE": 64.3
+ "deltaE": 37
},
{
"left": "function",
"right": "string",
"minDeltaE": 8,
- "deltaE": 36.8
+ "deltaE": 48.1
},
{
"left": "keyword",
@@ -564,12 +570,12 @@
"minBackgroundHueDistance": null,
"roles": {
"keyword": {
- "color": "#b34732",
- "hue": 9.8,
- "saturation": 0.563,
+ "color": "#b6442f",
+ "hue": 9.3,
+ "saturation": 0.59,
"lightness": 0.449,
- "contrast": 4.14,
- "backgroundHueDistance": 25.1
+ "contrast": 4.15,
+ "backgroundHueDistance": 25.5
},
"tag": {
"color": "#77291e",
@@ -610,31 +616,31 @@
"minBackgroundHueDistance": null,
"roles": {
"method": {
- "color": "#a0684b",
- "hue": 20.5,
- "saturation": 0.362,
- "lightness": 0.461,
- "contrast": 3.5,
- "backgroundHueDistance": 14.4
+ "color": "#a46746",
+ "hue": 21.1,
+ "saturation": 0.402,
+ "lightness": 0.459,
+ "contrast": 3.47,
+ "backgroundHueDistance": 13.8
}
}
},
- "livedInSage": {
+ "warmWheatLiteral": {
"hueBand": [
- 85,
- 105
+ 28,
+ 45
],
- "minSaturation": 0.14,
+ "minSaturation": 0.3,
"minContrast": null,
"minBackgroundHueDistance": null,
"roles": {
"string": {
- "color": "#6a7f58",
- "hue": 92.3,
- "saturation": 0.181,
- "lightness": 0.422,
- "contrast": 3.34,
- "backgroundHueDistance": 57.5
+ "color": "#7a4f21",
+ "hue": 31,
+ "saturation": 0.574,
+ "lightness": 0.304,
+ "contrast": 5.39,
+ "backgroundHueDistance": 3.8
}
}
},
@@ -667,34 +673,40 @@
"minBackgroundHueDistance": null,
"roles": {
"number": {
- "color": "#9a637e",
- "hue": 330.5,
- "saturation": 0.217,
- "lightness": 0.496,
- "contrast": 3.59,
- "backgroundHueDistance": 64.3
+ "color": "#8d5e75",
+ "hue": 330.6,
+ "saturation": 0.2,
+ "lightness": 0.461,
+ "contrast": 4.01,
+ "backgroundHueDistance": 64.2
}
}
}
},
"criticalPairs": [
+ {
+ "left": "property",
+ "right": "string",
+ "minDeltaE": 24,
+ "deltaE": 28.9
+ },
{
"left": "keyword",
"right": "string",
"minDeltaE": 9,
- "deltaE": 60.3
+ "deltaE": 32.4
},
{
"left": "function",
"right": "string",
"minDeltaE": 8,
- "deltaE": 43.2
+ "deltaE": 62.2
},
{
"left": "keyword",
"right": "number",
"minDeltaE": 8,
- "deltaE": 43.9
+ "deltaE": 47.2
}
]
}
diff --git a/reports/color-contract-audit.md b/reports/color-contract-audit.md
index c6b95d5..c2cf774 100644
--- a/reports/color-contract-audit.md
+++ b/reports/color-contract-audit.md
@@ -62,33 +62,35 @@ Hearthlight over paper and warm tooling with bounded cool structure.
| emberOrange | tag | #af513b | 11.4 | 0.496 | 3.33 | 11.1 |
| controlledMineralBlue | function | #6f94a4 | 198.1 | 0.226 | 5.29 | 175.6 |
| leatherMethodWarmth | method | #ad6a45 | 21.3 | 0.43 | 4.04 | 1.2 |
-| livedInSage | string | #8eaa79 | 94.3 | 0.224 | 6.71 | 71.8 |
+| warmWheatLiteral | string | #c39b69 | 33.3 | 0.429 | 6.74 | 10.8 |
| dryOchreType | type | #99904c | 53 | 0.336 | 5.29 | 30.5 |
| dustyConstant | number | #aa7a94 | 327.5 | 0.22 | 4.86 | 55 |
| Pair | deltaE | Minimum |
| --- | ---: | ---: |
-| keyword / string | 64.3 | 9 |
-| function / string | 36.8 | 8 |
+| property / string | 26 | 24 |
+| keyword / string | 37 | 9 |
+| function / string | 48.1 | 8 |
| keyword / number | 47.2 | 8 |
### light
| Lane | Role | Color | Hue | Sat | Contrast | Bg Hue Gap |
| --- | --- | --- | ---: | ---: | ---: | ---: |
-| emberOrange | keyword | #b34732 | 9.8 | 0.563 | 4.14 | 25.1 |
+| emberOrange | keyword | #b6442f | 9.3 | 0.59 | 4.15 | 25.5 |
| emberOrange | tag | #77291e | 7.4 | 0.597 | 7.55 | 27.4 |
| controlledMineralBlue | function | #2e6f8f | 199.8 | 0.513 | 4.23 | 165 |
-| leatherMethodWarmth | method | #a0684b | 20.5 | 0.362 | 3.5 | 14.4 |
-| livedInSage | string | #6a7f58 | 92.3 | 0.181 | 3.34 | 57.5 |
+| leatherMethodWarmth | method | #a46746 | 21.1 | 0.402 | 3.47 | 13.8 |
+| warmWheatLiteral | string | #7a4f21 | 31 | 0.574 | 5.39 | 3.8 |
| dryOchreType | type | #736a18 | 54.1 | 0.655 | 4.21 | 19.2 |
-| dustyConstant | number | #9a637e | 330.5 | 0.217 | 3.59 | 64.3 |
+| dustyConstant | number | #8d5e75 | 330.6 | 0.2 | 4.01 | 64.2 |
| Pair | deltaE | Minimum |
| --- | ---: | ---: |
-| keyword / string | 60.3 | 9 |
-| function / string | 43.2 | 8 |
-| keyword / number | 43.9 | 8 |
+| property / string | 28.9 | 24 |
+| keyword / string | 32.4 | 9 |
+| function / string | 62.2 | 8 |
+| keyword / number | 47.2 | 8 |
## Issues
diff --git a/reports/obsidian-hero-manifest.json b/reports/obsidian-hero-manifest.json
index 4f14bba..70163c3 100644
--- a/reports/obsidian-hero-manifest.json
+++ b/reports/obsidian-hero-manifest.json
@@ -1,5 +1,5 @@
{
- "renderer": "obsidian-functional-markdown-v3",
- "hash": "791a68ac74326cefcd2cfd455f93a4e4d46a6f7913b69bafd2b197567d1fcc5d",
+ "renderer": "obsidian-functional-markdown-v7",
+ "hash": "cdd0a7ec1adace21120da690a979f761c246813acd0abeb4e722b20b2f7c95a7",
"width": 1600
}
diff --git a/reports/obsidian-screenshot-manifest.json b/reports/obsidian-screenshot-manifest.json
index de86e97..e9702e7 100644
--- a/reports/obsidian-screenshot-manifest.json
+++ b/reports/obsidian-screenshot-manifest.json
@@ -1,6 +1,6 @@
{
- "renderer": "obsidian-functional-markdown-v3",
- "hash": "dbe9bf49545670940eb8f60e00f19542aa36411d4df64da7364936448a90109f",
+ "renderer": "obsidian-functional-markdown-v7",
+ "hash": "49a2e2ce479fe49dedf87a2462f23d8ba28edd30f1b8f9e1f1823931dad07504",
"width": 512,
"height": 288
}
diff --git a/reports/preview-manifest.json b/reports/preview-manifest.json
index e2a19a3..21a4326 100644
--- a/reports/preview-manifest.json
+++ b/reports/preview-manifest.json
@@ -1,30 +1,287 @@
{
- "schemaVersion": 3,
+ "schemaVersion": 6,
"generator": "scripts/generate-preview-images.mjs",
- "renderer": "marketplace-source-color-v10",
- "generatorSourceSha256": "7e650964f7bde6689a6549490d3ca3512d2da18d803a8f8789c7021a783648b3",
- "promoSpecSha256": "0f551ce9ccad45b45e1c517d38862c978a42c37fcbeb05369a7fb05215869749",
+ "renderer": "semantic-materials-v1",
+ "generatorSourceSha256": "e9af2eefc8cc9d2f9fca4e47fd90302557c2fcba3d43e04fb53ada52d5fad057",
+ "brandSystem": {
+ "id": "semantic-materials-v1",
+ "source": "scripts/marketing/brand-system.mjs",
+ "sourceSha256": "6be90a3a41fb656023d1a7b04e9c16b2f861d8f565a58a1b8ca92d56e85491d4",
+ "templateComponentsSource": "scripts/marketing/template-components.mjs",
+ "templateComponentsSourceSha256": "c4717477067b40023634b264c6f028e9a6eba7fd9f655caf33d416408a84eb97"
+ },
+ "assetSpec": {
+ "source": "products/hearthcode/marketing-assets.json",
+ "sourceSha256": "e8b0e359b56e1947fde9b21f9ef4435a35c9bfc151541bb3effde73104a2fa05"
+ },
+ "promoSpecSha256": "0091fed3159115deb439d8a67889ac08a65ac2b776e2264a885090bee02b574b",
+ "colorFidelity": {
+ "policy": "theme-source-only-v1",
+ "structuralColorRule": "Signature fields and swatches use unmodified shipped theme tokens; structural overlays use only alpha or mixes of those tokens.",
+ "themes": {
+ "moss-dark": {
+ "source": "themes/moss-dark.json",
+ "sourceSha256": "37c7185a46cea02b22e2933c694705d4cc0b54f854f3455ac93833a8581730c9",
+ "colors": {
+ "surface": "#1b1d1a",
+ "foreground": "#d2bea2",
+ "keyword": "#d4b84f",
+ "function": "#93ce75",
+ "type": "#68adc1",
+ "string": "#caa46c",
+ "property": "#8ebb7f",
+ "operator": "#979087"
+ }
+ },
+ "moss-light": {
+ "source": "themes/moss-light.json",
+ "sourceSha256": "9c7c90c955bb815caabc0176ff57d981271b75c1cbad5d771a7a41ee8c40bc9c",
+ "colors": {
+ "surface": "#e7e5d8",
+ "foreground": "#342d28",
+ "keyword": "#83640f",
+ "function": "#3e752b",
+ "type": "#237296",
+ "string": "#7e5a2a",
+ "property": "#46703f",
+ "operator": "#5f5c57"
+ }
+ },
+ "ember-dark": {
+ "source": "themes/ember-dark.json",
+ "sourceSha256": "f40fee3e10b50188479836fedbd67f69a028628fc3f5a627a085f51ed779b80a",
+ "colors": {
+ "surface": "#1f1a17",
+ "foreground": "#d3c9b8",
+ "keyword": "#ca5b41",
+ "function": "#6f94a4",
+ "type": "#99904c",
+ "string": "#c39b69",
+ "property": "#788058",
+ "operator": "#9c978f"
+ }
+ },
+ "ember-light": {
+ "source": "themes/ember-light.json",
+ "sourceSha256": "96b4d6823ffad6ef6669e1e71e4c0e0c1d27b07c190a2e96899ad0b8d837c988",
+ "colors": {
+ "surface": "#ecdfcd",
+ "foreground": "#30261b",
+ "keyword": "#b6442f",
+ "function": "#2e6f8f",
+ "type": "#736a18",
+ "string": "#7a4f21",
+ "property": "#5c6447",
+ "operator": "#64675f"
+ }
+ }
+ }
+ },
"canvas": {
"width": 1600,
"height": 900
},
+ "formats": {
+ "readme-wide": {
+ "width": 1600,
+ "height": 900,
+ "safeInset": 56
+ },
+ "github-social": {
+ "width": 1280,
+ "height": 640,
+ "safeInset": 48
+ },
+ "site-og": {
+ "width": 1200,
+ "height": 630,
+ "safeInset": 44
+ },
+ "social-square": {
+ "width": 1200,
+ "height": 1200,
+ "safeInset": 64
+ },
+ "social-portrait": {
+ "width": 1080,
+ "height": 1350,
+ "safeInset": 64
+ },
+ "social-story": {
+ "width": 1080,
+ "height": 1920,
+ "safeInset": 72
+ }
+ },
"editorHero": {
- "inputSha256": "5207b23067012e92d85a8f9cab9777b3d8fa8aa9f311568b2cf9c49aa8f88894",
+ "inputSha256": "61ab730236ad026141ec1a74459fa8a26773b755d60528cfb7e55458628b3c96",
"outputs": [
- "extension/images/preview-editor-hero.png"
+ "extension/images/editor-moss-dark-light.png"
]
},
"contrastImage": {
- "inputSha256": "85e91ada8ec0983b14fb24421acfe0b6933796634534787e7dec037d835af9d0",
+ "inputSha256": "5bef617f92b895664ae33cc50669709e618b0cb5776b391572e0a4773b872463",
"outputs": [
- "extension/images/preview-contrast-v2.png",
- "public/previews/preview-contrast-v2.png"
+ "extension/images/family-overview.png",
+ "public/previews/family-overview.png"
]
},
"forgeWorkflow": {
- "inputSha256": "4b92979a687ebd66e2f62dff503196dce113fedf6c207a58dc53f8b8c55fb50f",
+ "inputSha256": "45f6adea44eea64bd3b1de56b8bbee142f4b52a319dd7686ba01ec32fff53cc4",
"outputs": [
- "extension/images/preview-forge-workflow.png"
+ "extension/images/theme-forge-workflow.png"
]
- }
+ },
+ "directionAtlas": {
+ "inputSha256": "d732e746d325b73b969e340d201d44c20adcb291f51b25890c986a241a27cbab",
+ "outputs": [
+ "docs/marketing/direction-atlas.png"
+ ]
+ },
+ "platformCoverage": {
+ "inputSha256": "6c615cdd984c87607b35bea7345335d1f50bd02540154efe9e280c5c4e6412c2",
+ "outputs": [
+ "docs/marketing/platform-coverage.png"
+ ]
+ },
+ "mossSurfaces": {
+ "inputSha256": "d918014be5df3342c40befc5471d54d6236a207c0e477714655eb65fadf06516",
+ "outputs": [
+ "docs/marketing/moss-surfaces.png"
+ ]
+ },
+ "socialCard": {
+ "inputSha256": "b1360fddbf782108d7146d729f5cded31160e1acce811120a078d8658c3fc082",
+ "canvas": {
+ "width": 1200,
+ "height": 630,
+ "safeInset": 44
+ },
+ "outputs": [
+ "public/og-hearth.png"
+ ]
+ },
+ "githubSocial": {
+ "inputSha256": "2c6f4a697bb8882cbf621e4f650cc8b1735a6c2e27d288eea8283da4f0842c83",
+ "canvas": {
+ "width": 1280,
+ "height": 640,
+ "safeInset": 48
+ },
+ "outputs": [
+ "docs/marketing/exports/github-social.png"
+ ]
+ },
+ "familySquare": {
+ "inputSha256": "2f2a6afdae0791f6ffccffdeda99c3919cda12ca01d6de06f711745bbc3efacc",
+ "canvas": {
+ "width": 1200,
+ "height": 1200,
+ "safeInset": 64
+ },
+ "outputs": [
+ "docs/marketing/exports/family-square.png"
+ ]
+ },
+ "familyPortrait": {
+ "inputSha256": "24780ed8ba0835db2c311efbb9fb627843c98c015dab9fe4dc8936555ba9477e",
+ "canvas": {
+ "width": 1080,
+ "height": 1350,
+ "safeInset": 64
+ },
+ "outputs": [
+ "docs/marketing/exports/family-portrait.png"
+ ]
+ },
+ "familyStory": {
+ "inputSha256": "4bc22268e4fe31d3596eb151f71a9ca954256585e00965d9a64e2ba845e3d2fd",
+ "canvas": {
+ "width": 1080,
+ "height": 1920,
+ "safeInset": 72
+ },
+ "outputs": [
+ "docs/marketing/exports/family-story.png"
+ ]
+ },
+ "emberSquare": {
+ "inputSha256": "ffec4b38931bd90dd61a26444ff9d24eab00c952108bba737eb12965c4f363db",
+ "canvas": {
+ "width": 1200,
+ "height": 1200,
+ "safeInset": 64
+ },
+ "outputs": [
+ "docs/marketing/exports/ember-square.png"
+ ]
+ },
+ "mossSquare": {
+ "inputSha256": "48dc77f1757043e94cf8199ea46a7459c428d3e2420c02c971f111e21ed6238f",
+ "canvas": {
+ "width": 1200,
+ "height": 1200,
+ "safeInset": 64
+ },
+ "outputs": [
+ "docs/marketing/exports/moss-square.png"
+ ]
+ },
+ "zedPlatform": {
+ "inputSha256": "e66d31cd2e0c543ae92d1bdf6cd4a94f384e421577d093079f1aeea67c555d47",
+ "canvas": {
+ "width": 1600,
+ "height": 900,
+ "safeInset": 56
+ },
+ "outputs": [
+ "zed/images/hearthcode-zed.png"
+ ]
+ },
+ "terminalPlatform": {
+ "inputSha256": "4d6b9c00ef9c0ddc0d7c8e8bc2a39d62971d57ad0ac314332336d31ede8cd7b7",
+ "canvas": {
+ "width": 1600,
+ "height": 900,
+ "safeInset": 56
+ },
+ "outputs": [
+ "terminal/hearthcode-terminal.png"
+ ]
+ },
+ "managedAssets": [
+ {
+ "id": "obsidian-hero",
+ "generator": "scripts/generate-obsidian-app-theme.mjs",
+ "format": "readme-wide",
+ "channels": [
+ "github-readme",
+ "obsidian-mirror-readme"
+ ],
+ "outputs": [
+ "docs/marketing/obsidian-hero.png"
+ ],
+ "canvas": {
+ "width": 1600,
+ "height": 900,
+ "safeInset": 56
+ },
+ "outputSha256": {
+ "docs/marketing/obsidian-hero.png": "3b1bf84aafc5bad0cc12a18783b0c5ea0f020707173334e2ff9d22ca72a0fa2f"
+ }
+ },
+ {
+ "id": "obsidian-community-screenshot",
+ "generator": "scripts/generate-obsidian-app-theme.mjs",
+ "channels": [
+ "obsidian-community-themes"
+ ],
+ "outputs": [
+ "obsidian/app-theme/screenshot.png"
+ ],
+ "outputSha256": {
+ "obsidian/app-theme/screenshot.png": "eb633045b5d5ce63dec5a911ab5b65bd695969bde59d28aea9ff389f61736f64"
+ }
+ }
+ ]
}
diff --git a/schemas/marketing-assets.schema.json b/schemas/marketing-assets.schema.json
new file mode 100644
index 0000000..5301da8
--- /dev/null
+++ b/schemas/marketing-assets.schema.json
@@ -0,0 +1,104 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "marketing-assets.schema.json",
+ "title": "HearthCode marketing asset matrix",
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["schemaVersion", "brandSystem", "renderer", "formats", "assets", "managedAssets"],
+ "properties": {
+ "schemaVersion": { "const": 1 },
+ "brandSystem": { "type": "string", "minLength": 1 },
+ "renderer": { "type": "string", "minLength": 1 },
+ "formats": {
+ "type": "object",
+ "minProperties": 1,
+ "additionalProperties": { "$ref": "#/definitions/format" }
+ },
+ "assets": {
+ "type": "array",
+ "minItems": 1,
+ "items": { "$ref": "#/definitions/generatedAsset" }
+ },
+ "managedAssets": {
+ "type": "array",
+ "items": { "$ref": "#/definitions/managedAsset" }
+ }
+ },
+ "definitions": {
+ "format": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["width", "height", "safeInset"],
+ "properties": {
+ "width": { "type": "integer", "minimum": 256 },
+ "height": { "type": "integer", "minimum": 256 },
+ "safeInset": { "type": "integer", "minimum": 16 }
+ }
+ },
+ "generatedAsset": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["id", "template", "format", "channels", "outputs"],
+ "properties": {
+ "id": { "type": "string", "pattern": "^[a-z0-9-]+$" },
+ "template": {
+ "enum": [
+ "family",
+ "editor-proof",
+ "forge-workflow",
+ "direction-atlas",
+ "platform-coverage",
+ "moss-surfaces",
+ "direction-card",
+ "channel-proof"
+ ]
+ },
+ "format": { "type": "string", "minLength": 1 },
+ "composition": {
+ "enum": [
+ "semantic-rift-wide",
+ "editorial-square",
+ "stacked-directions",
+ "campaign-story"
+ ]
+ },
+ "schemeId": { "enum": ["ember", "moss"] },
+ "channelId": { "enum": ["vscode", "openvsx", "zed", "terminal", "obsidian"] },
+ "channels": {
+ "type": "array",
+ "minItems": 1,
+ "uniqueItems": true,
+ "items": { "type": "string", "minLength": 1 }
+ },
+ "outputs": {
+ "type": "array",
+ "minItems": 1,
+ "uniqueItems": true,
+ "items": { "type": "string", "pattern": "^[^/].*\\.png$" }
+ }
+ }
+ },
+ "managedAsset": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["id", "generator", "channels", "outputs"],
+ "properties": {
+ "id": { "type": "string", "pattern": "^[a-z0-9-]+$" },
+ "generator": { "type": "string", "minLength": 1 },
+ "format": { "type": "string", "minLength": 1 },
+ "channels": {
+ "type": "array",
+ "minItems": 1,
+ "uniqueItems": true,
+ "items": { "type": "string", "minLength": 1 }
+ },
+ "outputs": {
+ "type": "array",
+ "minItems": 1,
+ "uniqueItems": true,
+ "items": { "type": "string", "pattern": "^[^/].*\\.png$" }
+ }
+ }
+ }
+ }
+}
diff --git a/schemas/preview.schema.json b/schemas/preview.schema.json
index e7fa06b..a3a744a 100644
--- a/schemas/preview.schema.json
+++ b/schemas/preview.schema.json
@@ -27,6 +27,113 @@
"dark": { "type": "string" },
"light": { "type": "string" }
}
+ },
+ "marketing": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "familyHeadline",
+ "familySubheadline",
+ "directionHeadline",
+ "directionSubheadline",
+ "platformHeadline",
+ "platformSubheadline",
+ "mossSurfaceHeadline",
+ "mossSurfaceSubheadline",
+ "editorsHeadline",
+ "editorsSubheadline",
+ "obsidianHeadline",
+ "obsidianSubheadline",
+ "terminalHeadline",
+ "terminalSubheadline",
+ "forgeHeadline",
+ "forgeSubheadline"
+ ],
+ "properties": {
+ "familyHeadline": { "type": "string" },
+ "familySubheadline": { "type": "string" },
+ "directionHeadline": { "type": "string" },
+ "directionSubheadline": { "type": "string" },
+ "platformHeadline": { "type": "string" },
+ "platformSubheadline": { "type": "string" },
+ "mossSurfaceHeadline": { "type": "string" },
+ "mossSurfaceSubheadline": { "type": "string" },
+ "editorsHeadline": { "type": "string" },
+ "editorsSubheadline": { "type": "string" },
+ "obsidianHeadline": { "type": "string" },
+ "obsidianSubheadline": { "type": "string" },
+ "terminalHeadline": { "type": "string" },
+ "terminalSubheadline": { "type": "string" },
+ "forgeHeadline": { "type": "string" },
+ "forgeSubheadline": { "type": "string" },
+ "directions": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["ember", "moss"],
+ "properties": {
+ "ember": { "$ref": "#/definitions/directionCopy" },
+ "moss": { "$ref": "#/definitions/directionCopy" }
+ }
+ }
+ }
+ },
+ "samples": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["family", "editors", "obsidian", "terminal", "forge"],
+ "properties": {
+ "family": { "$ref": "#/definitions/sample" },
+ "editors": { "$ref": "#/definitions/sample" },
+ "obsidian": { "$ref": "#/definitions/sample" },
+ "terminal": { "$ref": "#/definitions/sample" },
+ "forge": { "$ref": "#/definitions/sample" }
+ }
+ }
+ },
+ "definitions": {
+ "directionCopy": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "summary",
+ "chips",
+ "comment",
+ "sampleFunction",
+ "sampleVariable",
+ "sampleString",
+ "sampleValue",
+ "directionLabel",
+ "focusLabel"
+ ],
+ "properties": {
+ "summary": { "type": "string", "minLength": 1 },
+ "chips": {
+ "type": "array",
+ "minItems": 3,
+ "maxItems": 3,
+ "items": { "type": "string", "minLength": 1 }
+ },
+ "comment": { "type": "string", "minLength": 1 },
+ "sampleFunction": { "type": "string", "minLength": 1 },
+ "sampleVariable": { "type": "string", "minLength": 1 },
+ "sampleString": { "type": "string", "minLength": 1 },
+ "sampleValue": { "type": "string", "minLength": 1 },
+ "directionLabel": { "type": "string", "minLength": 1 },
+ "focusLabel": { "type": "string", "minLength": 1 }
+ }
+ },
+ "sample": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["language", "lines"],
+ "properties": {
+ "language": { "type": "string", "minLength": 1 },
+ "lines": {
+ "type": "array",
+ "minItems": 1,
+ "items": { "type": "string", "minLength": 1 }
+ }
+ }
}
}
}
diff --git a/schemas/product.schema.json b/schemas/product.schema.json
index d105ddf..6ac6a5e 100644
--- a/schemas/product.schema.json
+++ b/schemas/product.schema.json
@@ -106,7 +106,35 @@
"vscode": { "type": "boolean" },
"openvsx": { "type": "boolean" },
"obsidian": { "type": "boolean" },
- "terminal": { "type": "boolean" }
+ "terminal": { "type": "boolean" },
+ "zed": { "type": "boolean" }
+ }
+ },
+ "channelAvailability": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["schemeIds", "variantIds", "capabilityIds"],
+ "properties": {
+ "schemeIds": {
+ "type": "array",
+ "minItems": 1,
+ "uniqueItems": true,
+ "items": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" }
+ },
+ "variantIds": {
+ "type": "array",
+ "minItems": 1,
+ "uniqueItems": true,
+ "items": { "type": "string", "enum": ["dark", "light"] }
+ },
+ "capabilityIds": {
+ "type": "array",
+ "uniqueItems": true,
+ "items": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" }
+ }
+ }
}
}
}
diff --git a/schemas/release.schema.json b/schemas/release.schema.json
index 4bdb4ff..66f5b4c 100644
--- a/schemas/release.schema.json
+++ b/schemas/release.schema.json
@@ -83,6 +83,18 @@
"items": { "type": "string", "enum": ["dark", "light"] }
}
}
+ },
+ "zedExtension": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["id", "name", "authors", "description", "repository"],
+ "properties": {
+ "id": { "type": "string", "pattern": "^[a-z][a-z0-9-]*-theme$" },
+ "name": { "type": "string" },
+ "authors": { "type": "array", "minItems": 1, "items": { "type": "string" } },
+ "description": { "type": "string" },
+ "repository": { "type": "string", "pattern": "^https://" }
+ }
}
}
}
diff --git a/scripts/audit-source-schema.mjs b/scripts/audit-source-schema.mjs
index 11b89b6..84ce370 100644
--- a/scripts/audit-source-schema.mjs
+++ b/scripts/audit-source-schema.mjs
@@ -23,6 +23,7 @@ const schemas = {
product: loadSchema('product.schema.json'),
release: loadSchema('release.schema.json'),
preview: loadSchema('preview.schema.json'),
+ marketingAssets: loadSchema('marketing-assets.schema.json'),
}
const schemaFailures = []
@@ -69,6 +70,8 @@ for (const dir of productDirs) {
const product = checkFile(path.join(base, 'product.json'), schemas.product)
checkFile(path.join(base, 'release.json'), schemas.release)
checkFile(path.join(base, 'preview.json'), schemas.preview)
+ const marketingAssetsPath = path.join(base, 'marketing-assets.json')
+ if (fs.existsSync(marketingAssetsPath)) checkFile(marketingAssetsPath, schemas.marketingAssets)
// Cross-field invariants a flat schema can't express.
if (product) {
diff --git a/scripts/check-built-site.mjs b/scripts/check-built-site.mjs
index b8355c5..7d493b9 100644
--- a/scripts/check-built-site.mjs
+++ b/scripts/check-built-site.mjs
@@ -44,7 +44,11 @@ for (const expected of [
'data-variant-id="dark"',
'data-variant-id="light"',
'data-install="openvsx"',
+ 'data-install="zed"',
'data-install="obsidian"',
+ 'data-install="terminal"',
+ 'href="https://zed.dev/extensions/hearthcode-theme"',
+ 'href="https://github.com/hearth-code/HearthTheme/tree/main/terminal"',
'data-install-placement="hero"',
'data-hero-forge-link',
'data-forge-base="/forge"',
@@ -53,6 +57,27 @@ for (const expected of [
if (!home.includes(expected)) fail(`Home page is missing ${expected}`)
}
+const installAnchors = home.match(/]*>/g) || []
+const equalChannelIds = ['vscode', 'openvsx', 'zed', 'obsidian', 'terminal']
+for (const placement of ['hero', 'final']) {
+ const cards = installAnchors.filter((anchor) => (
+ anchor.includes('install-channel-card')
+ && anchor.includes(`data-install-placement="${placement}"`)
+ ))
+ const ids = cards.map((anchor) => anchor.match(/data-install="([^"]+)"/)?.[1]).sort()
+ const expectedIds = [...equalChannelIds].sort()
+ if (JSON.stringify(ids) !== JSON.stringify(expectedIds)) {
+ fail(`${placement} install grid must contain one equal card for each editor channel`)
+ }
+ if (new Set(cards.map((anchor) => anchor.match(/class="([^"]+)"/)?.[1])).size !== 1) {
+ fail(`${placement} install grid must use the same card class for every editor channel`)
+ }
+}
+
+for (const legacyPriorityClass of ['hero-action--primary', 'final-install-primary']) {
+ if (home.includes(legacyPriorityClass)) fail(`Home page still contains priority styling: ${legacyPriorityClass}`)
+}
+
const forge = read('dist/forge/index.html')
for (const expected of [
'data-quality-label="CONTRACT OK"',
diff --git a/scripts/check-preview-clean.mjs b/scripts/check-preview-clean.mjs
index 1beb6e3..1bdef17 100644
--- a/scripts/check-preview-clean.mjs
+++ b/scripts/check-preview-clean.mjs
@@ -43,7 +43,7 @@ function main() {
for (const file of dirty) {
process.stderr.write(` - ${file}\n`)
}
- process.stderr.write('\nRun: pnpm run preview:generate && git add reports/preview-manifest.json extension/images public/previews\n')
+ process.stderr.write('\nRun: pnpm run preview:generate && git add reports/preview-manifest.json extension/images public/previews docs/marketing public/og-hearth.png zed/images terminal/hearthcode-terminal.png\n')
process.exit(1)
}
diff --git a/scripts/check-sync-clean.mjs b/scripts/check-sync-clean.mjs
index 399909a..656dcee 100644
--- a/scripts/check-sync-clean.mjs
+++ b/scripts/check-sync-clean.mjs
@@ -15,6 +15,7 @@ const SYNCED_PATHS = [
'terminal/kitty',
'terminal/alacritty',
'terminal/iterm2',
+ 'zed/extension',
'docs/color-language-report.md',
'docs/color-language-contract-checklist.md',
'docs/color-language-contract-review.md',
diff --git a/scripts/color-system.mjs b/scripts/color-system.mjs
index 0197aaa..cd8c493 100644
--- a/scripts/color-system.mjs
+++ b/scripts/color-system.mjs
@@ -813,6 +813,28 @@ export function loadColorProductManifest(productId = COLOR_SYSTEM_PRODUCT_ID) {
Object.entries(data.channels).map(([channelId, enabled]) => [String(channelId || '').trim(), Boolean(enabled)])
)
: {}
+ const channelAvailability = data.channelAvailability && typeof data.channelAvailability === 'object' && !Array.isArray(data.channelAvailability)
+ ? Object.fromEntries(
+ Object.entries(data.channelAvailability).map(([channelIdRaw, entry]) => {
+ const channelId = String(channelIdRaw || '').trim()
+ assert(channelId, `${productPath}: channelAvailability has an invalid channel id`)
+ assert(entry && typeof entry === 'object' && !Array.isArray(entry), `${productPath}: channelAvailability.${channelId} must be an object`)
+ const schemeIds = toStringList(entry.schemeIds, `${productPath}: channelAvailability.${channelId}.schemeIds`)
+ const channelVariantIds = toStringList(entry.variantIds, `${productPath}: channelAvailability.${channelId}.variantIds`)
+ const capabilityIds = toStringList(entry.capabilityIds, `${productPath}: channelAvailability.${channelId}.capabilityIds`)
+ assert(channels[channelId] === true, `${productPath}: channelAvailability.${channelId} must reference an enabled channel`)
+ assert(schemeIds.length > 0, `${productPath}: channelAvailability.${channelId}.schemeIds must not be empty`)
+ assert(channelVariantIds.length > 0, `${productPath}: channelAvailability.${channelId}.variantIds must not be empty`)
+ for (const schemeId of schemeIds) {
+ assert(supportedSchemeIds.includes(schemeId), `${productPath}: channelAvailability.${channelId} references unsupported scheme "${schemeId}"`)
+ }
+ for (const variantId of channelVariantIds) {
+ assert(variantIds.has(variantId), `${productPath}: channelAvailability.${channelId} references unregistered variant "${variantId}"`)
+ }
+ return [channelId, { schemeIds, variantIds: channelVariantIds, capabilityIds }]
+ }),
+ )
+ : {}
assert(id === expectedProductId, `${productPath}: id must match product "${expectedProductId}"`)
assert(name, `${productPath}: name is required`)
@@ -871,6 +893,7 @@ export function loadColorProductManifest(productId = COLOR_SYSTEM_PRODUCT_ID) {
featuredThemes,
brandFlavorIds,
channels,
+ channelAvailability,
}
}
@@ -878,6 +901,14 @@ export function loadColorProductPreviewConfig(productId = COLOR_SYSTEM_PRODUCT_I
const { productPreviewPath } = getProductPaths(productId)
const data = readJson(productPreviewPath)
assert(data && typeof data === 'object' && !Array.isArray(data), `${productPreviewPath} must be an object`)
+ const toStringList = (value, label) => {
+ assert(Array.isArray(value), `${label} must be an array`)
+ return value.map((item, index) => {
+ const normalized = String(item || '').trimEnd()
+ assert(normalized.trim(), `${label}[${index}] must be a non-empty string`)
+ return normalized
+ })
+ }
const badgeLabel = String(data.badgeLabel || '').trim()
const headline = String(data.headline || '').trim()
@@ -892,6 +923,49 @@ export function loadColorProductPreviewConfig(productId = COLOR_SYSTEM_PRODUCT_I
Object.entries(data.variantNames).map(([variantId, label]) => [String(variantId || '').trim(), String(label || '').trim()])
)
: {}
+ const marketing = {}
+ if (data.marketing && typeof data.marketing === 'object' && !Array.isArray(data.marketing)) {
+ for (const [keyRaw, value] of Object.entries(data.marketing)) {
+ const key = String(keyRaw || '').trim()
+ assert(key, `${productPreviewPath}: marketing has an invalid key`)
+ if (key !== 'directions') {
+ marketing[key] = String(value || '').trim()
+ continue
+ }
+
+ assert(value && typeof value === 'object' && !Array.isArray(value), `${productPreviewPath}: marketing.directions must be an object`)
+ marketing.directions = Object.fromEntries(Object.entries(value).map(([schemeIdRaw, direction]) => {
+ const schemeId = String(schemeIdRaw || '').trim()
+ assert(schemeId, `${productPreviewPath}: marketing.directions has an invalid scheme id`)
+ assert(direction && typeof direction === 'object' && !Array.isArray(direction), `${productPreviewPath}: marketing.directions.${schemeId} must be an object`)
+ return [schemeId, {
+ summary: String(direction.summary || '').trim(),
+ chips: toStringList(direction.chips, `${productPreviewPath}: marketing.directions.${schemeId}.chips`),
+ comment: String(direction.comment || '').trimEnd(),
+ sampleFunction: String(direction.sampleFunction || '').trim(),
+ sampleVariable: String(direction.sampleVariable || '').trim(),
+ sampleString: String(direction.sampleString || '').trim(),
+ sampleValue: String(direction.sampleValue || '').trim(),
+ directionLabel: String(direction.directionLabel || '').trim(),
+ focusLabel: String(direction.focusLabel || '').trim(),
+ }]
+ }))
+ }
+ }
+ const samples = data.samples && typeof data.samples === 'object' && !Array.isArray(data.samples)
+ ? Object.fromEntries(
+ Object.entries(data.samples).map(([sampleIdRaw, sample]) => {
+ const sampleId = String(sampleIdRaw || '').trim()
+ assert(sampleId, `${productPreviewPath}: samples has an invalid id`)
+ assert(sample && typeof sample === 'object' && !Array.isArray(sample), `${productPreviewPath}: samples.${sampleId} must be an object`)
+ const language = String(sample.language || '').trim()
+ const lines = toStringList(sample.lines, `${productPreviewPath}: samples.${sampleId}.lines`)
+ assert(language, `${productPreviewPath}: samples.${sampleId}.language is required`)
+ assert(lines.length > 0, `${productPreviewPath}: samples.${sampleId}.lines must not be empty`)
+ return [sampleId, { language, lines }]
+ }),
+ )
+ : {}
assert(headline, `${productPreviewPath}: headline is required`)
assert(subheadline, `${productPreviewPath}: subheadline is required`)
@@ -900,7 +974,6 @@ export function loadColorProductPreviewConfig(productId = COLOR_SYSTEM_PRODUCT_I
for (const variant of loadColorSystemVariants().variants) {
assert(variantNames[variant.id], `${productPreviewPath}: variantNames.${variant.id} is required`)
}
-
return {
schemaVersion: Number(data.schemaVersion || 1),
badgeLabel,
@@ -908,6 +981,8 @@ export function loadColorProductPreviewConfig(productId = COLOR_SYSTEM_PRODUCT_I
subheadline,
familyLabels,
variantNames,
+ marketing,
+ samples,
}
}
@@ -918,6 +993,7 @@ export function loadColorProductReleaseConfig(productId = COLOR_SYSTEM_PRODUCT_I
assert(data.site && typeof data.site === 'object' && !Array.isArray(data.site), `${productReleasePath}: site must be an object`)
assert(data.vscodeExtension && typeof data.vscodeExtension === 'object' && !Array.isArray(data.vscodeExtension), `${productReleasePath}: vscodeExtension must be an object`)
assert(data.obsidianAppTheme && typeof data.obsidianAppTheme === 'object' && !Array.isArray(data.obsidianAppTheme), `${productReleasePath}: obsidianAppTheme must be an object`)
+ assert(data.zedExtension == null || (typeof data.zedExtension === 'object' && !Array.isArray(data.zedExtension)), `${productReleasePath}: zedExtension must be an object when set`)
const toStringList = (value, label) => {
assert(Array.isArray(value), `${label} must be an array`)
@@ -966,6 +1042,16 @@ export function loadColorProductReleaseConfig(productId = COLOR_SYSTEM_PRODUCT_I
: [],
}
+ const zedExtension = data.zedExtension
+ ? {
+ id: String(data.zedExtension.id || '').trim(),
+ name: String(data.zedExtension.name || '').trim(),
+ authors: toStringList(data.zedExtension.authors, `${productReleasePath}: zedExtension.authors`),
+ description: String(data.zedExtension.description || '').trim(),
+ repository: String(data.zedExtension.repository || '').trim(),
+ }
+ : null
+
assert(site.titleDescriptor, `${productReleasePath}: site.titleDescriptor is required`)
assert(site.ogImagePath, `${productReleasePath}: site.ogImagePath is required`)
assert(vscodeExtension.name, `${productReleasePath}: vscodeExtension.name is required`)
@@ -987,12 +1073,20 @@ export function loadColorProductReleaseConfig(productId = COLOR_SYSTEM_PRODUCT_I
assert(obsidianAppTheme.screenshotPath, `${productReleasePath}: obsidianAppTheme.screenshotPath is required`)
assert(obsidianAppTheme.packageBasename, `${productReleasePath}: obsidianAppTheme.packageBasename is required`)
assert(obsidianAppTheme.modes.length > 0, `${productReleasePath}: obsidianAppTheme.modes must include at least one mode`)
+ if (zedExtension) {
+ assert(/^[a-z][a-z0-9-]*-theme$/.test(zedExtension.id), `${productReleasePath}: zedExtension.id must end in "-theme"`)
+ assert(zedExtension.name, `${productReleasePath}: zedExtension.name is required`)
+ assert(!/\b(?:zed|extension)\b/i.test(zedExtension.name), `${productReleasePath}: zedExtension.name cannot contain "Zed" or "extension"`)
+ assert(zedExtension.description, `${productReleasePath}: zedExtension.description is required`)
+ assert(/^https:\/\//.test(zedExtension.repository), `${productReleasePath}: zedExtension.repository must use HTTPS`)
+ }
return {
schemaVersion: Number(data.schemaVersion || 1),
site,
vscodeExtension,
obsidianAppTheme,
+ zedExtension,
}
}
diff --git a/scripts/content-sync-audit.mjs b/scripts/content-sync-audit.mjs
index 5df95cd..2119bce 100644
--- a/scripts/content-sync-audit.mjs
+++ b/scripts/content-sync-audit.mjs
@@ -568,15 +568,26 @@ function validateReadmePreviewAssets() {
const editorHeroOutput = normalizeRepoPath(manifest.editorHero?.outputs?.[0])
const contrastOutput = normalizeRepoPath(manifest.contrastImage?.outputs?.[0])
const forgeWorkflowOutput = normalizeRepoPath(manifest.forgeWorkflow?.outputs?.[0])
+ const directionAtlasOutput = normalizeRepoPath(manifest.directionAtlas?.outputs?.[0])
+ const platformCoverageOutput = normalizeRepoPath(manifest.platformCoverage?.outputs?.[0])
- if (!editorHeroOutput || !contrastOutput || !forgeWorkflowOutput) {
- addIssue(`${PREVIEW_MANIFEST}: missing editor hero, contrast, or Forge workflow output`)
+ if (!editorHeroOutput || !contrastOutput || !forgeWorkflowOutput || !directionAtlasOutput || !platformCoverageOutput) {
+ addIssue(`${PREVIEW_MANIFEST}: missing editor hero, family overview, direction atlas, platform coverage, or Forge workflow output`)
return
}
- const expectedRootPreviewPaths = [`./${contrastOutput}`]
+ const expectedRootPreviewPaths = [
+ `./${contrastOutput}`,
+ `./${directionAtlasOutput}`,
+ `./${platformCoverageOutput}`,
+ `./${forgeWorkflowOutput}`,
+ ]
const extensionPath = (path) => path.startsWith('extension/') ? path.slice('extension/'.length) : path
const expectedExtensionPreviewPaths = [editorHeroOutput, contrastOutput, forgeWorkflowOutput].map(extensionPath)
+ const generatedPreviewPaths = new Set([
+ ...expectedRootPreviewPaths,
+ ...expectedExtensionPreviewPaths,
+ ])
const expectedReadmes = [
{
@@ -601,7 +612,7 @@ function validateReadmePreviewAssets() {
const readme = readText(spec.file)
if (!readme) continue
- const previewPaths = collectMarkdownImagePaths(readme).filter((path) => /preview-.*\.png$/i.test(path))
+ const previewPaths = collectMarkdownImagePaths(readme).filter((path) => generatedPreviewPaths.has(path))
if (previewPaths.length !== spec.previewPaths.length) {
addIssue(
`${spec.file}: expected ${spec.previewPaths.length} generated preview images, got ${previewPaths.length}`
diff --git a/scripts/generate-obsidian-app-theme.mjs b/scripts/generate-obsidian-app-theme.mjs
index 6d59534..bd0e6fe 100644
--- a/scripts/generate-obsidian-app-theme.mjs
+++ b/scripts/generate-obsidian-app-theme.mjs
@@ -6,6 +6,7 @@ import { getReleaseVersion } from './release-metadata.mjs'
import { loadColorProductReleaseConfig } from './color-system.mjs'
import {
RENDERER_VERSION,
+ buildObsidianHeroSvg,
buildObsidianScreenshotSvg,
renderObsidianScreenshotBuffer,
renderObsidianHeroBuffer,
@@ -206,13 +207,14 @@ async function generateScreenshot() {
return changed
}
-// Reproducible README marketing hero: the SAME theme-driven diagonal SVG as the
-// community screenshot, rasterized larger. Guarded by the same renderer+SVG hash
-// so check:sync stays clean across machines (the PNG raster is not byte-stable).
+// Reproducible README product proof: the same theme-driven functional Markdown
+// surface as the community screenshot, enlarged into one Dark/Light mode-cut
+// application frame. Guarded by a renderer+SVG hash so check:sync stays clean
+// across machines (the PNG raster is not byte-stable).
async function generateHero() {
let svg = null
try {
- svg = buildObsidianScreenshotSvg(readFileSync(THEME_CSS_PATH, 'utf8'))
+ svg = buildObsidianHeroSvg(readFileSync(THEME_CSS_PATH, 'utf8'))
} catch {
svg = null
}
diff --git a/scripts/generate-preview-images.mjs b/scripts/generate-preview-images.mjs
index f2ae7c4..049369e 100644
--- a/scripts/generate-preview-images.mjs
+++ b/scripts/generate-preview-images.mjs
@@ -3,28 +3,52 @@ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node
import { dirname, join } from "node:path";
import sharp from "sharp";
import { getThemeMetaListForSchemeId, loadColorProductManifest, loadColorProductPreviewConfig, loadColorSchemeManifestById, loadRoleAdapters } from "./color-system.mjs";
-
-const WIDTH = 1600;
-const HEIGHT = 900;
+import { BRAND_SYSTEM, escapeXml, mixHex, normalizeHex, withAlpha } from "./marketing/brand-system.mjs";
+import { indexMarketingAssets, loadMarketingAssetSpec } from "./marketing/asset-spec.mjs";
+import { assertSemanticRiftLayout, buildSemanticRiftLayout, buildTornPaperGeometry, renderDistressedText, renderFieldGuideFooter, renderFieldGuideGrid, renderFieldGuideHeader, renderMaterialTexture, renderRegistrationMarks, renderTornPaperSeam } from "./marketing/template-components.mjs";
+
+const MARKETING_SPEC = loadMarketingAssetSpec();
+const MARKETING_ASSETS = indexMarketingAssets(MARKETING_SPEC);
+const WIDTH = MARKETING_SPEC.formats["readme-wide"].width;
+const HEIGHT = MARKETING_SPEC.formats["readme-wide"].height;
const OUTPUT_DIR = join("extension", "images");
const WEBSITE_OUTPUT_DIR = join("public", "previews");
+const MARKETING_OUTPUT_DIR = join("docs", "marketing");
const MANIFEST_PATH = join("reports", "preview-manifest.json");
-const PREVIEW_RENDERER = "marketplace-source-color-v10";
+const PREVIEW_RENDERER = MARKETING_SPEC.renderer;
const GENERATOR_SOURCE_SHA256 = createHash("sha256").update(readFileSync(new URL(import.meta.url))).digest("hex");
+const BRAND_SYSTEM_SOURCE_SHA256 = createHash("sha256").update(readFileSync(new URL("./marketing/brand-system.mjs", import.meta.url))).digest("hex");
+const TEMPLATE_COMPONENTS_SOURCE_SHA256 = createHash("sha256").update(readFileSync(new URL("./marketing/template-components.mjs", import.meta.url))).digest("hex");
+const ASSET_SPEC_SOURCE_SHA256 = createHash("sha256").update(readFileSync(new URL("../products/hearthcode/marketing-assets.json", import.meta.url))).digest("hex");
const PRODUCT = loadColorProductManifest();
const PREVIEW = loadColorProductPreviewConfig();
-const CONTRAST_OUTPUTS = [
+function assetOutputs(id) {
+ const asset = MARKETING_ASSETS[id];
+ if (!asset) throw new Error(`Missing marketing asset spec: ${id}`);
+ return asset.outputs;
+}
+
+const CONTRAST_OUTPUTS = assetOutputs("family-readme");
+const EDITOR_HERO_OUTPUTS = assetOutputs("editor-marketplace");
+const FORGE_WORKFLOW_OUTPUTS = assetOutputs("forge-marketplace");
+const DIRECTION_ATLAS_OUTPUTS = assetOutputs("direction-atlas");
+const PLATFORM_COVERAGE_OUTPUTS = assetOutputs("platform-coverage");
+const MOSS_SURFACES_OUTPUTS = assetOutputs("moss-surfaces");
+const GITHUB_SOCIAL_OUTPUTS = assetOutputs("github-social");
+const OG_OUTPUTS = assetOutputs("site-og");
+const FAMILY_SQUARE_OUTPUTS = assetOutputs("family-square");
+const FAMILY_PORTRAIT_OUTPUTS = assetOutputs("family-portrait");
+const FAMILY_STORY_OUTPUTS = assetOutputs("family-story");
+const EMBER_SQUARE_OUTPUTS = assetOutputs("ember-square");
+const MOSS_SQUARE_OUTPUTS = assetOutputs("moss-square");
+const ZED_PLATFORM_OUTPUTS = assetOutputs("zed-platform");
+const TERMINAL_PLATFORM_OUTPUTS = assetOutputs("terminal-platform");
+const LEGACY_PREVIEW_OUTPUTS = [
join(OUTPUT_DIR, "preview-contrast-v2.png"),
join(WEBSITE_OUTPUT_DIR, "preview-contrast-v2.png"),
-];
-const EDITOR_HERO_OUTPUTS = [
join(OUTPUT_DIR, "preview-editor-hero.png"),
-];
-const FORGE_WORKFLOW_OUTPUTS = [
join(OUTPUT_DIR, "preview-forge-workflow.png"),
-];
-const LEGACY_PREVIEW_OUTPUTS = [
join(WEBSITE_OUTPUT_DIR, "preview-dark.png"),
join(WEBSITE_OUTPUT_DIR, "preview-dark-soft.png"),
join(WEBSITE_OUTPUT_DIR, "preview-light.png"),
@@ -47,30 +71,7 @@ const ROLE_SCOPES = Object.fromEntries(loadRoleAdapters().map((role) => [role.id
const FLAVOR_IDS = PRODUCT.brandFlavorIds?.length ? PRODUCT.brandFlavorIds : PRODUCT.supportedSchemeIds;
const FLAVORS_BY_ID = Object.fromEntries(FLAVOR_IDS.map((schemeId) => [schemeId, loadColorSchemeManifestById(schemeId)]));
const VARIANTS_BY_SCHEME_ID = Object.fromEntries(FLAVOR_IDS.map((schemeId) => [schemeId, getThemeMetaListForSchemeId(schemeId)]));
-const FLAVOR_PREVIEW_COPY = {
- ember: {
- summary: "Warm charcoal and paper, with ember control flow and cool callable anchors.",
- chips: ["warm neutrals", "ember control", "denim callables"],
- comment: "// warm-neutral structure with cool anchors",
- sampleFunction: "renderTheme",
- sampleVariable: "theme",
- sampleString: '"ember"',
- sampleValue: '"hearth"',
- directionLabel: "WARM-NEUTRAL DIRECTION",
- focusLabel: "COOL CALLABLES",
- },
- moss: {
- summary: "Dry charcoal and paper, with clearer lane split and greener callable structure.",
- chips: ["dry paper", "editorial lane split", "lichen callables"],
- comment: "// dry editorial lanes with calm callables",
- sampleFunction: "routeSignal",
- sampleVariable: "palette",
- sampleString: '"moss"',
- sampleValue: '"field"',
- directionLabel: "DRY EDITORIAL DIRECTION",
- focusLabel: "GREEN CALLABLES",
- },
-};
+const FLAVOR_PREVIEW_COPY = PREVIEW.marketing?.directions || {};
function buildFallbackThemeMeta() {
return FLAVOR_IDS.map((schemeId) => {
@@ -136,63 +137,6 @@ function writeJsonIfChanged(path, data) {
return true;
}
-function escapeXml(input) {
- return String(input)
- .replaceAll("&", "&")
- .replaceAll("<", "<")
- .replaceAll(">", ">")
- .replaceAll('"', """)
- .replaceAll("'", "'");
-}
-
-function normalizeHex(hex) {
- if (typeof hex !== "string") return null;
- const value = hex.trim().toLowerCase();
- if (/^#[0-9a-f]{6}$/i.test(value)) return value;
- if (/^#[0-9a-f]{8}$/i.test(value)) return value.slice(0, 7);
- return null;
-}
-
-function hexToRgb(hex) {
- const normalized = normalizeHex(hex);
- if (!normalized) return null;
- const value = normalized.slice(1);
- return [
- Number.parseInt(value.slice(0, 2), 16),
- Number.parseInt(value.slice(2, 4), 16),
- Number.parseInt(value.slice(4, 6), 16),
- ];
-}
-
-function rgbToHex(rgb) {
- return `#${rgb
- .map((channel) =>
- Math.max(0, Math.min(255, Math.round(channel)))
- .toString(16)
- .padStart(2, "0"))
- .join("")}`;
-}
-
-function mixHex(a, b, weight = 0.5) {
- const rgbA = hexToRgb(a);
- const rgbB = hexToRgb(b);
- if (!rgbA && !rgbB) return "#000000";
- if (!rgbA) return normalizeHex(b) ?? "#000000";
- if (!rgbB) return normalizeHex(a) ?? "#000000";
- const t = Math.max(0, Math.min(1, weight));
- return rgbToHex([
- rgbA[0] + (rgbB[0] - rgbA[0]) * t,
- rgbA[1] + (rgbB[1] - rgbA[1]) * t,
- rgbA[2] + (rgbB[2] - rgbA[2]) * t,
- ]);
-}
-
-function withAlpha(hex, alpha) {
- const rgb = hexToRgb(hex);
- if (!rgb) return `rgba(0, 0, 0, ${alpha})`;
- return `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${alpha})`;
-}
-
function themeColor(theme, key, fallback) {
return normalizeHex(theme.colors?.[key]) ?? fallback;
}
@@ -339,6 +283,43 @@ function roleColor(theme, role) {
return resolvePreviewStyle(theme, role).color;
}
+function requiredThemeColor(meta, key) {
+ const color = normalizeHex(meta.theme?.colors?.[key]);
+ if (!color) {
+ throw new Error(`Preview color contract: ${meta.id} is missing theme color "${key}"`);
+ }
+ return color;
+}
+
+function requiredRoleColor(meta, role) {
+ const color = normalizeHex(roleColor(meta.theme, role));
+ if (!color) {
+ throw new Error(`Preview color contract: ${meta.id} is missing syntax role "${role}"`);
+ }
+ return color;
+}
+
+function buildMarketingColorContract(themes) {
+ return {
+ policy: "theme-source-only-v1",
+ structuralColorRule: "Signature fields and swatches use unmodified shipped theme tokens; structural overlays use only alpha or mixes of those tokens.",
+ themes: Object.fromEntries(themes.map((meta) => [meta.id, {
+ source: toPosixPath(meta.file),
+ sourceSha256: meta.sourceSha256,
+ colors: {
+ surface: requiredThemeColor(meta, "editor.background"),
+ foreground: requiredThemeColor(meta, "editor.foreground"),
+ keyword: requiredRoleColor(meta, "keyword"),
+ function: requiredRoleColor(meta, "function"),
+ type: requiredRoleColor(meta, "type"),
+ string: requiredRoleColor(meta, "string"),
+ property: requiredRoleColor(meta, "property"),
+ operator: requiredRoleColor(meta, "operator"),
+ },
+ }])),
+ };
+}
+
function fontWeightForStyle(style, fallback = 600) {
return style?.fontStyle?.includes("bold") ? 700 : fallback;
}
@@ -443,7 +424,8 @@ function getFlagshipThemes(themes) {
}
function orderThemesForPreview(themes) {
- const flavorOrder = new Map(FLAVOR_IDS.map((schemeId, index) => [schemeId, index]));
+ const previewFlavorIds = [...new Set(["ember", "moss", ...FLAVOR_IDS])];
+ const flavorOrder = new Map(previewFlavorIds.map((schemeId, index) => [schemeId, index]));
const variantOrder = new Map([
["dark", 0],
["light", 1],
@@ -680,142 +662,475 @@ function renderPaletteSwatchStrip({ x, y, colors }) {
`).join("");
}
-function renderThemeCodeCard({ meta, x, y, width, height }) {
- const theme = meta.theme;
- const copy = getFlavorPreviewCopy(meta.schemeId);
- const bg = themeColor(theme, "editor.background", "#211d1a");
- const fg = themeColor(theme, "editor.foreground", "#d3c9b8");
- const panel = themeColor(
- theme,
- "editorGroupHeader.tabsBackground",
- mixHex(bg, meta.isDark ? "#000000" : "#ffffff", meta.isDark ? 0.07 : 0.03),
- );
- const cardFill = bg;
- const border = mixHex(themeColor(theme, "tab.border", "#35302b"), fg, meta.isDark ? 0.14 : 0.24);
- const muted = mixHex(fg, bg, 0.48);
- const labelMuted = mixHex(fg, bg, 0.6);
- const surface = themeColor(theme, "editor.background", bg);
- const surfaceStroke = withAlpha(fg, meta.isDark ? 0.1 : 0.16);
- const accentKeyword = roleColor(theme, "keyword");
- const accentCallable = roleColor(theme, "function");
- const accentType = roleColor(theme, "type");
- const accentString = roleColor(theme, "string");
- const accent = meta.schemeId === "moss" ? accentCallable : accentKeyword;
- const previewX = x + 24;
- const previewY = y + 132;
- const previewWidth = width - 48;
- const previewHeight = 104;
- const codeX = previewX + 52;
- const lineNumberX = previewX + 16;
- const codeY = previewY + 10;
- const lineHeight = 22;
- const fontSize = 13.5;
- const rows = [
- [{ role: "keyword", text: "type " }, { role: "type", text: meta.flavor.name }, { role: "plain", text: " = {" }],
- [{ role: "plain", text: " " }, { role: "property", text: "mode" }, { role: "plain", text: ": " }, { role: "string", text: `"${meta.variantId}"` }, { role: "plain", text: ";" }],
- [{ role: "plain", text: " " }, { role: "property", text: "accent" }, { role: "plain", text: ": " }, { role: "function", text: "createTheme" }, { role: "plain", text: "();" }],
- [{ role: "plain", text: "};" }],
+function buildFamilySampleLines(meta) {
+ const lines = [
+ [{ role: "keyword", text: "const " }, { role: "variable.readonly", text: "theme" }, { role: "operator", text: " = " }, { role: "punctuation", text: "{" }],
+ [{ role: "plain", text: " " }, { role: "property", text: "direction" }, { role: "operator", text: ": " }, { role: "string", text: `"${meta.schemeId}"` }, { role: "punctuation", text: "," }],
+ [{ role: "plain", text: " " }, { role: "property", text: "mode" }, { role: "operator", text: ": " }, { role: "string", text: `"${meta.variantId}"` }, { role: "punctuation", text: "," }],
+ [{ role: "punctuation", text: "} " }, { role: "keyword", text: "as const" }, { role: "punctuation", text: ";" }],
];
+ const expected = (PREVIEW.samples?.family?.lines || []).map((line) => line
+ .replaceAll("{direction}", meta.schemeId)
+ .replaceAll("{mode}", meta.variantId));
+ const actual = lines.map((segments) => segments.map((segment) => segment.text).join(""));
+ if (JSON.stringify(actual) !== JSON.stringify(expected)) {
+ throw new Error(`Preview sample contract: generated family sample for ${meta.id} does not match products/hearthcode/preview.json`);
+ }
+ return lines;
+}
- const renderedRows = rows.map((segments, index) => {
- const rowY = codeY + index * lineHeight;
- return `
- ${index + 1}
- ${renderCodeLine({ theme, segments, x: codeX, y: rowY, fontSize })}
- `;
- }).join("");
+function buildEditorSampleLines(meta) {
+ const lines = [
+ [{ role: "keyword", text: "type " }, { role: "type", text: "ThemePreview" }, { role: "operator", text: " = " }, { role: "punctuation", text: "{" }],
+ [{ role: "plain", text: " " }, { role: "property", text: "direction" }, { role: "operator", text: ": " }, { role: "string", text: '"ember"' }, { role: "operator", text: " | " }, { role: "string", text: '"moss"' }, { role: "punctuation", text: ";" }],
+ [{ role: "plain", text: " " }, { role: "property", text: "mode" }, { role: "operator", text: ": " }, { role: "string", text: '"dark"' }, { role: "operator", text: " | " }, { role: "string", text: '"light"' }, { role: "punctuation", text: ";" }],
+ [{ role: "punctuation", text: "};" }],
+ [{ role: "keyword", text: "const " }, { role: "variable.readonly", text: "theme" }, { role: "operator", text: ": " }, { role: "type", text: "ThemePreview" }, { role: "operator", text: " = " }, { role: "punctuation", text: "{" }],
+ [{ role: "plain", text: " " }, { role: "property", text: "direction" }, { role: "operator", text: ": " }, { role: "string", text: '"moss"' }, { role: "punctuation", text: "," }],
+ [{ role: "plain", text: " " }, { role: "property", text: "mode" }, { role: "operator", text: ": " }, { role: "string", text: `"${meta.variantId}"` }, { role: "punctuation", text: "," }],
+ [{ role: "punctuation", text: "};" }],
+ ];
+ const expected = (PREVIEW.samples?.editors?.lines || []).map((line) => line.replaceAll("{mode}", meta.variantId));
+ const actual = lines.map((segments) => segments.map((segment) => segment.text).join(""));
+ if (JSON.stringify(actual) !== JSON.stringify(expected)) {
+ throw new Error(`Preview sample contract: generated editor sample for ${meta.id} does not match products/hearthcode/preview.json`);
+ }
+ return lines;
+}
+function renderRiftSample({ meta, x, y, layout }) {
+ const foreground = requiredThemeColor(meta, "editor.foreground");
+ const lines = buildFamilySampleLines(meta);
+ const { fontSize, lineHeight, labelSize, swatchOffset, swatchStep, swatchWidth, swatchHeight } = layout.sample;
+ const tokenColors = ["keyword", "function", "type", "string", "property", "operator"]
+ .map((role) => requiredRoleColor(meta, role));
+ const label = `${meta.flavor.name} ${meta.climateLabel}`.toUpperCase();
return `
-
-
-
-
-
-
-
- palette.ts
-
- ${escapeXml(`${meta.flavor.name} / ${meta.climateLabel}`.toUpperCase())}
- ${escapeXml(meta.shortName)}
-
- ${escapeXml(meta.isDark ? "DARK" : "LIGHT")}
-
-
- ${renderedRows}
-
- ${renderPaletteSwatchStrip({
- x: x + 24,
- y: y + height - 36,
- colors: [accentKeyword, accentCallable, accentType, accentString],
- })}
- ${escapeXml(copy.focusLabel)}
+ ${escapeXml(label)}
+ ${lines.map((segments, index) => renderCodeLine({
+ theme: meta.theme,
+ segments,
+ x,
+ y: y + labelSize + 15 * layout.scale + index * lineHeight,
+ fontSize,
+ })).join("")}
+ ${tokenColors.map((color, index) => ``).join("")}
`;
}
-function renderPromoBoardSvg({ themes }) {
- const orderedThemes = orderThemesForPreview(themes);
- const darkThemes = orderedThemes.filter((theme) => theme.isDark);
- const lightThemes = orderedThemes.filter((theme) => !theme.isDark);
- const defaultThemeMeta = orderedThemes.find((theme) => theme.isDefaultTheme) || orderedThemes[0];
- const emberTheme = orderedThemes.find((theme) => theme.schemeId === "ember") || defaultThemeMeta;
- const mossTheme = orderedThemes.find((theme) => theme.schemeId === "moss") || orderedThemes.find((theme) => theme !== emberTheme) || defaultThemeMeta;
- const emberDark = darkThemes.find((theme) => theme.schemeId === "ember") || emberTheme;
- const mossDark = darkThemes.find((theme) => theme.schemeId === "moss") || mossTheme;
- const emberSurface = themeColor(emberDark.theme, "editor.background", "#1f1a17");
- const mossSurface = themeColor(mossDark.theme, "editor.background", "#1b1d1a");
- const boardFg = themeColor(mossDark.theme, "editor.foreground", "#d2bea2");
- const boardMuted = mixHex(boardFg, mossSurface, 0.46);
- const gradientId = "promo-board-bg";
- const warmGlowId = "promo-board-glow-warm";
- const mossGlowId = "promo-board-glow-moss";
- const darkLabel = PREVIEW.familyLabels?.dark || "DARK STARTING POINTS";
- const lightLabel = PREVIEW.familyLabels?.light || "LIGHT STARTING POINTS";
- const cardWidth = 748;
- const cardHeight = 286;
- const leftX = 40;
- const rightX = leftX + cardWidth + 24;
- const topRowY = 206;
- const bottomRowY = 536;
+function renderSemanticRiftSvg({ themes, width = WIDTH, height = HEIGHT }) {
+ const layout = buildSemanticRiftLayout({ width, height });
+ assertSemanticRiftLayout(layout);
+ const { scale, splitY } = layout;
+ const emberDark = getPreviewTheme(themes, "ember", "dark");
+ const mossDark = getPreviewTheme(themes, "moss", "dark");
+ const emberLight = getPreviewTheme(themes, "ember", "light");
+ const mossLight = getPreviewTheme(themes, "moss", "light");
+ const emberDarkBg = requiredThemeColor(emberDark, "editor.background");
+ const mossDarkBg = requiredThemeColor(mossDark, "editor.background");
+ const emberLightBg = requiredThemeColor(emberLight, "editor.background");
+ const mossLightBg = requiredThemeColor(mossLight, "editor.background");
+ const emberDarkFg = requiredThemeColor(emberDark, "editor.foreground");
+ const mossDarkFg = requiredThemeColor(mossDark, "editor.foreground");
+ const emberLightFg = requiredThemeColor(emberLight, "editor.foreground");
+ const mossLightFg = requiredThemeColor(mossLight, "editor.foreground");
+ const emberAccent = requiredRoleColor(emberDark, "keyword");
+ const mossAccent = requiredRoleColor(mossDark, "function");
+ const headline = PREVIEW.marketing?.familyHeadline || "EMBER / MOSS";
+ const subheadline = PREVIEW.marketing?.familySubheadline || "FOUR THEMES. ONE COLOR LANGUAGE.";
+ const [emberWord = "EMBER", mossWord = "MOSS"] = headline.split("/").map((word) => word.trim());
+ const subheadlineMatch = subheadline.match(/^(.+?\.)\s+(.+)$/);
+ const subheadlineLeft = subheadlineMatch?.[1] || subheadline;
+ const subheadlineRight = subheadlineMatch?.[2] || "";
+ const leftX = layout.title.emberX;
+ const rightX = layout.sample.rightX;
+ const darkSampleY = layout.sample.darkY;
+ const lightSampleY = layout.sample.lightY;
+ const headlineY = layout.title.y;
+ const headlineSize = layout.title.fontSize;
+ const mossX = layout.title.mossX;
+ const tearGeometry = buildTornPaperGeometry({
+ controlPoints: layout.tearControlPoints,
+ seed: 83,
+ segmentLength: 13 * scale,
+ jitter: 6 * scale,
+ paperWidth: 14 * scale,
+ widthVariation: 0.84,
+ });
+ const pointsString = (points) => points.map((point) => `${point.x.toFixed(2)},${point.y.toFixed(2)}`).join(" ");
+ const emberMask = `0,0 ${pointsString(tearGeometry.sideB)} 0,${height}`;
+ const mossMask = `${pointsString([tearGeometry.sideA[0], { x: width, y: 0 }, { x: width, y: height }, ...[...tearGeometry.sideA].reverse()])}`;
return `
-
-
-
-
-
+
{t(lang, 'surfaces.vscode.body')}
-
- {t(lang, 'hero.cta.install')}
-
+
@@ -419,6 +436,42 @@ const webBudgetRatio = '4.5:1'
border-bottom-color: var(--hearth-kicker-accent);
}
+ .surfaces-editor-links {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 0.45rem;
+ }
+
+ .surfaces-editor-link {
+ --channel-icon-size: 1.6rem;
+ display: flex;
+ align-items: center;
+ gap: 0.42rem;
+ min-width: 0;
+ padding: 0.38rem;
+ border: 1px solid var(--hearth-panel-edge);
+ background: color-mix(in srgb, var(--hearth-panel-face) 92%, var(--hearth-bg));
+ color: var(--hearth-proof-metric-title);
+ font-family: var(--font-ui);
+ font-size: 0.68rem;
+ font-weight: 650;
+ line-height: 1.2;
+ text-decoration: none;
+ transition:
+ border-color 180ms var(--hearth-ease),
+ background-color 180ms var(--hearth-ease);
+ }
+
+ .surfaces-editor-link:hover {
+ border-color: var(--hearth-kicker-accent);
+ background: color-mix(in srgb, var(--hearth-kicker-accent) 8%, var(--hearth-panel-face));
+ }
+
+ .surfaces-editor-link:focus-visible {
+ outline: 2px solid var(--hearth-kicker-accent);
+ outline-offset: 2px;
+ }
+
.surfaces-note {
margin: clamp(1.3rem, 2.6vw, 2rem) 0 0;
}
diff --git a/src/data/product.ts b/src/data/product.ts
index ee3afad..9467baa 100644
--- a/src/data/product.ts
+++ b/src/data/product.ts
@@ -4,13 +4,13 @@ export const productData = {
"id": "hearthcode-theme",
"name": "HearthCode Theme",
"displayName": "HearthCode Theme",
- "summary": "A theme family for code interfaces."
+ "summary": "Warmth or structure, with one semantic color system."
},
"product": {
"id": "hearthcode",
"name": "HearthCode",
"displayName": "HearthCode Theme",
- "summary": "A low-glare theme family for code interfaces.",
+ "summary": "A calibrated Ember and Moss theme family for code interfaces.",
"author": {
"name": "HearthCode",
"url": "https://theme.hearthcode.dev"
@@ -129,10 +129,80 @@ export const productData = {
"isDark": false
}
],
+ "channels": {
+ "website": true,
+ "vscode": true,
+ "openvsx": true,
+ "obsidian": true,
+ "terminal": true,
+ "zed": true
+ },
+ "channelAvailability": {
+ "vscode": {
+ "schemeIds": [
+ "ember",
+ "moss"
+ ],
+ "variantIds": [
+ "dark",
+ "light"
+ ],
+ "capabilityIds": [
+ "theme-forge"
+ ]
+ },
+ "openvsx": {
+ "schemeIds": [
+ "ember",
+ "moss"
+ ],
+ "variantIds": [
+ "dark",
+ "light"
+ ],
+ "capabilityIds": []
+ },
+ "zed": {
+ "schemeIds": [
+ "ember",
+ "moss"
+ ],
+ "variantIds": [
+ "dark",
+ "light"
+ ],
+ "capabilityIds": []
+ },
+ "terminal": {
+ "schemeIds": [
+ "ember",
+ "moss"
+ ],
+ "variantIds": [
+ "dark",
+ "light"
+ ],
+ "capabilityIds": [
+ "five-formats"
+ ]
+ },
+ "obsidian": {
+ "schemeIds": [
+ "moss"
+ ],
+ "variantIds": [
+ "dark",
+ "light"
+ ],
+ "capabilityIds": [
+ "style-settings"
+ ]
+ }
+ },
"site": {
"titleSuffix": "HearthCode",
- "titleDescriptor": "Color Language for Code Interfaces",
- "defaultTitle": "HearthCode — Color Language for Code Interfaces",
+ "titleDescriptor": "Ember and Moss Themes for Code Interfaces",
+ "defaultTitle": "HearthCode — Ember and Moss Themes for Code Interfaces",
"authorName": "HearthCode",
"metaPrefix": "hearthcode",
"hostLabel": "theme.hearthcode.dev",
@@ -145,7 +215,7 @@ export const productData = {
}
},
"release": {
- "version": "3.7.4"
+ "version": "3.7.7"
},
"extension": {
"itemName": "hearth-code.hearth-theme",
@@ -219,6 +289,8 @@ export const productData = {
"issuesUrl": "https://github.com/hearth-code/HearthTheme/issues",
"releasesUrl": "https://github.com/hearth-code/HearthTheme/releases",
"obsidianUrl": "https://community.obsidian.md/themes/hearthcode",
+ "zedUrl": "https://zed.dev/extensions/hearthcode-theme",
+ "terminalUrl": "https://github.com/hearth-code/HearthTheme/tree/main/terminal",
"changelogUrl": "https://github.com/hearth-code/HearthTheme/blob/main/extension/CHANGELOG.md",
"licenseUrl": "https://github.com/hearth-code/HearthTheme/blob/main/LICENSE",
"docsRootUrl": "https://github.com/hearth-code/HearthTheme/blob/main/docs",
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 96d3022..58d9fd5 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -3,8 +3,8 @@
"nav.themes": "Themes",
"nav.forge": "Forge",
"nav.install": "Install",
- "hero.kicker": "Theme cockpit",
- "hero.subtitle": "Pick Ember or Moss, preview the shipped theme live, then install it where you already work.",
+ "hero.kicker": "Two materials. One semantic system.",
+ "hero.subtitle": "Warmth or structure. Meaning stays clear. Choose Ember or Moss, compare Dark and Light live, then install it where you work.",
"hero.flow.preview": "Preview theme",
"hero.flow.customize": "Customize color",
"hero.flow.install": "Choose where to install",
@@ -48,9 +48,10 @@
"surfaces.kicker": "Every surface",
"surfaces.title": "One color language, everywhere you work",
"surfaces.subtitle": "There is exactly one semantic system: comments always recede, control flow always leans forward. Changing surfaces never changes meaning.",
- "surfaces.vscode.title": "VS Code & VSX editors",
+ "surfaces.vscode.title": "Code editors",
"surfaces.vscode.meta": "directions × modes",
- "surfaces.vscode.body": "Ember and Moss each ship dark and light builds on the VS Code Marketplace and Open VSX, generated by the same pipeline.",
+ "surfaces.vscode.body": "Ember and Moss each ship dark and light builds for VS Code, Open VSX-compatible editors, and Zed, generated by the same audited pipeline.",
+ "surfaces.editorChannels.label": "Code editor install channels",
"surfaces.obsidian.title": "Obsidian",
"surfaces.obsidian.meta": "direction",
"surfaces.obsidian.body": "The Obsidian theme ships the Moss direction in dark and light, so notes and long-form reading use the same semantic system.",
@@ -68,17 +69,21 @@
"surfaces.web.preview.copy": "Body text stays steady while key actions move forward; one token set governs interface and code.",
"surfaces.web.preview.code": "const result = audit(tokens)",
"surfaces.web.preview.status": "AUDIT PASS",
- "surfaces.availability.note": "Ember currently ships for the VS Code ecosystem and this site; the Obsidian theme is the Moss direction.",
+ "surfaces.availability.note": "Ember and Moss ship for VS Code, VSX editors, and Zed; the Obsidian theme uses the Moss direction.",
"hero.quick.flavor": "Direction",
"hero.quick.variant": "Primary mode",
"hero.cta.try": "Try in vscode.dev",
- "hero.cta.install": "Install in VS Code",
- "hero.cta.vscode.meta": "VS Code Marketplace",
- "hero.cta.openvsx": "Install for VSX Editors",
- "hero.cta.openvsx.meta": "Cursor · Windsurf · VSCodium",
- "hero.cta.obsidian": "Get Obsidian Theme",
- "hero.cta.obsidian.meta": "Moss · dark and light",
"hero.cta.star": "View on GitHub",
+ "channel.vscode.label": "VS Code",
+ "channel.vscode.meta": "Marketplace · Ember & Moss",
+ "channel.openvsx.label": "VSX Editors",
+ "channel.openvsx.meta": "Cursor · Windsurf · VSCodium",
+ "channel.zed.label": "Zed",
+ "channel.zed.meta": "Ember & Moss · dark and light",
+ "channel.obsidian.label": "Obsidian",
+ "channel.obsidian.meta": "Moss · dark and light",
+ "channel.terminal.label": "Terminals",
+ "channel.terminal.meta": "5 formats · Ember & Moss",
"preview.label": "Live Token Preview",
"forge.meta.title": "Theme Forge",
"forge.meta.description": "Choose a color direction in the browser and preview calibrated dark and light HearthCode themes live.",
@@ -133,10 +138,7 @@
"fit.climate.dark.label": "Dark",
"fit.climate.light.label": "Light",
"final.title": "Install HearthCode",
- "final.subtitle": "Choose the editor you already use. VS Code and Open VSX builds come from the same audited release.",
- "final.option.vscode.title": "VS Code Marketplace",
- "final.option.openvsx.body": "For Cursor, Windsurf, VSCodium, and other Open VSX-compatible editors.",
- "final.option.obsidian.body": "For notes and long-form reading in Obsidian with the same HearthCode direction.",
+ "final.subtitle": "Choose where you work. Every install channel is presented equally, with its real theme coverage.",
"footer.docs": "Docs",
"footer.changelog": "Changelog",
"footer.issues": "Issues",
diff --git a/src/i18n/ja.json b/src/i18n/ja.json
index eb99b30..a013628 100644
--- a/src/i18n/ja.json
+++ b/src/i18n/ja.json
@@ -3,8 +3,8 @@
"nav.themes": "テーマ",
"nav.forge": "Forge",
"nav.install": "インストール",
- "hero.kicker": "テーマ作業台",
- "hero.subtitle": "まず Ember か Moss を選び、公開中のテーマをその場で見て、そのまま使う場所へ入れられます。",
+ "hero.kicker": "二つの質感。一つの意味体系。",
+ "hero.subtitle": "温もりか、構造か。意味は明瞭なまま。Ember または Moss を選び、Dark と Light をライブ比較して、使う場所へインストールできます。",
"hero.flow.preview": "テーマを確認",
"hero.flow.customize": "主色を調整",
"hero.flow.install": "インストール先を選ぶ",
@@ -48,9 +48,10 @@
"surfaces.kicker": "マルチサーフェス",
"surfaces.title": "同じ色の言語を、作業するすべての場所へ",
"surfaces.subtitle": "意味体系はただひとつ。コメントは常に後退し、制御フローは常に前へ。場所が変わっても意味は変わりません。",
- "surfaces.vscode.title": "VS Code と VSX エディタ",
+ "surfaces.vscode.title": "コードエディタ",
"surfaces.vscode.meta": "方向 × モード",
- "surfaces.vscode.body": "Ember と Moss はそれぞれダークとライトを備え、VS Code Marketplace と Open VSX で公開。同じ生成パイプラインの出力です。",
+ "surfaces.vscode.body": "Ember と Moss はそれぞれダークとライトを備え、VS Code、Open VSX 互換エディタ、Zed 向けに同じ監査済みパイプラインから公開されます。",
+ "surfaces.editorChannels.label": "コードエディタのインストール先",
"surfaces.obsidian.title": "Obsidian",
"surfaces.obsidian.meta": "方向",
"surfaces.obsidian.body": "Obsidian テーマは Moss 方向のダーク / ライト両版。ノートも長文リーディングも同じ意味体系で読めます。",
@@ -68,17 +69,21 @@
"surfaces.web.preview.copy": "本文は安定し、重要な操作は明確に前へ。同じ token が UI とコードを統制します。",
"surfaces.web.preview.code": "const result = audit(tokens)",
"surfaces.web.preview.status": "監査合格",
- "surfaces.availability.note": "Ember は現在 VS Code エコシステムと本サイト向け。Obsidian テーマは Moss 方向です。",
+ "surfaces.availability.note": "Ember と Moss は VS Code、VSX エディタ、Zed 向けに公開。Obsidian テーマは Moss 方向です。",
"hero.quick.flavor": "方向",
"hero.quick.variant": "主プレビューモード",
"hero.cta.try": "vscode.dev で試す",
- "hero.cta.install": "VS Code にインストール",
- "hero.cta.vscode.meta": "VS Code Marketplace",
- "hero.cta.openvsx": "VSX エディタにインストール",
- "hero.cta.openvsx.meta": "Cursor · Windsurf · VSCodium",
- "hero.cta.obsidian": "Obsidian テーマを取得",
- "hero.cta.obsidian.meta": "Moss · ダーク / ライト",
"hero.cta.star": "GitHub で見る",
+ "channel.vscode.label": "VS Code",
+ "channel.vscode.meta": "Marketplace · Ember / Moss",
+ "channel.openvsx.label": "VSX エディタ",
+ "channel.openvsx.meta": "Cursor · Windsurf · VSCodium",
+ "channel.zed.label": "Zed",
+ "channel.zed.meta": "Ember / Moss · ダーク / ライト",
+ "channel.obsidian.label": "Obsidian",
+ "channel.obsidian.meta": "Moss · ダーク / ライト",
+ "channel.terminal.label": "ターミナル",
+ "channel.terminal.meta": "5 形式 · Ember / Moss",
"preview.label": "ライブトークンプレビュー",
"forge.meta.title": "Theme Forge",
"forge.meta.description": "ブラウザで HearthCode のカラー方向を選び、校正済みのダーク/ライトテーマをライブ表示します。",
@@ -133,10 +138,7 @@
"fit.climate.dark.label": "ダーク",
"fit.climate.light.label": "ライト",
"final.title": "HearthCode をインストール",
- "final.subtitle": "今使っているエディタを選んでください。VS Code 版と Open VSX 版は同じ監査済みリリースから生成されています。",
- "final.option.vscode.title": "VS Code Marketplace",
- "final.option.openvsx.body": "Cursor、Windsurf、VSCodium などの Open VSX 互換エディタ向けです。",
- "final.option.obsidian.body": "Obsidian でも同じ HearthCode の方向を使いたいときはこちらです。",
+ "final.subtitle": "使用する場所を選んでください。すべての導入先を同列に表示し、実際のテーマ対応範囲を明示します。",
"footer.docs": "ドキュメント",
"footer.changelog": "変更履歴",
"footer.issues": "Issue",
diff --git a/src/i18n/zh.json b/src/i18n/zh.json
index 84fb86c..e33b765 100644
--- a/src/i18n/zh.json
+++ b/src/i18n/zh.json
@@ -3,8 +3,8 @@
"nav.themes": "主题",
"nav.forge": "Forge",
"nav.install": "安装",
- "hero.kicker": "主题工作台",
- "hero.subtitle": "先选 Ember 或 Moss,直接看真实主题预览,然后装到你正在用的地方。",
+ "hero.kicker": "两种质感,一套语义系统。",
+ "hero.subtitle": "温暖或秩序,语义始终清晰。选择 Ember 或 Moss,实时比较 Dark 与 Light,再安装到你的工作平台。",
"hero.flow.preview": "预览主题",
"hero.flow.customize": "自定义主色",
"hero.flow.install": "选择安装渠道",
@@ -48,9 +48,10 @@
"surfaces.kicker": "多载体",
"surfaces.title": "同一套色彩语言,装在你工作的每个地方",
"surfaces.subtitle": "语义体系只有一套:注释永远退后,控制流永远前倾。换载体不换语义。",
- "surfaces.vscode.title": "VS Code 与 VSX 编辑器",
+ "surfaces.vscode.title": "代码编辑器",
"surfaces.vscode.meta": "方向 × 版本",
- "surfaces.vscode.body": "Ember 与 Moss 各配深浅两版,发布于 VS Code Marketplace 与 Open VSX,同一份生成管线产出。",
+ "surfaces.vscode.body": "Ember 与 Moss 各配深浅两版,发布于 VS Code、Open VSX 兼容编辑器和 Zed,全部来自同一条审计生成管线。",
+ "surfaces.editorChannels.label": "代码编辑器安装渠道",
"surfaces.obsidian.title": "Obsidian",
"surfaces.obsidian.meta": "方向",
"surfaces.obsidian.body": "Obsidian 主题采用 Moss 方向,深浅双版,笔记和长文阅读用同一套语义体系。",
@@ -68,17 +69,21 @@
"surfaces.web.preview.copy": "正文稳定,关键动作清晰前倾;同一组 token 同时约束界面和代码。",
"surfaces.web.preview.code": "const result = audit(tokens)",
"surfaces.web.preview.status": "审计通过",
- "surfaces.availability.note": "Ember 目前发布于 VS Code 生态与本网站;Obsidian 主题为 Moss 方向。",
+ "surfaces.availability.note": "Ember 与 Moss 均发布于 VS Code、VSX 编辑器和 Zed;Obsidian 主题采用 Moss 方向。",
"hero.quick.flavor": "方向",
"hero.quick.variant": "主预览版本",
"hero.cta.try": "在 vscode.dev 试用",
- "hero.cta.install": "在 VS Code 安装",
- "hero.cta.vscode.meta": "VS Code Marketplace",
- "hero.cta.openvsx": "在 VSX 编辑器安装",
- "hero.cta.openvsx.meta": "Cursor · Windsurf · VSCodium",
- "hero.cta.obsidian": "获取 Obsidian 主题",
- "hero.cta.obsidian.meta": "Moss · 深浅双版",
"hero.cta.star": "查看 GitHub",
+ "channel.vscode.label": "VS Code",
+ "channel.vscode.meta": "Marketplace · Ember 与 Moss",
+ "channel.openvsx.label": "VSX 编辑器",
+ "channel.openvsx.meta": "Cursor · Windsurf · VSCodium",
+ "channel.zed.label": "Zed",
+ "channel.zed.meta": "Ember 与 Moss · 深浅双版",
+ "channel.obsidian.label": "Obsidian",
+ "channel.obsidian.meta": "Moss · 深浅双版",
+ "channel.terminal.label": "终端",
+ "channel.terminal.meta": "5 种格式 · Ember 与 Moss",
"preview.label": "实时代码预览",
"forge.meta.title": "Theme Forge",
"forge.meta.description": "在浏览器里选择 HearthCode 的色彩方向,并实时预览校准后的深色和浅色主题。",
@@ -133,10 +138,7 @@
"fit.climate.dark.label": "深色",
"fit.climate.light.label": "浅色",
"final.title": "安装 HearthCode",
- "final.subtitle": "选择你正在使用的编辑器;VS Code 与 Open VSX 版本来自同一份审计发布。",
- "final.option.vscode.title": "VS Code Marketplace",
- "final.option.openvsx.body": "适合 Cursor、Windsurf、VSCodium 等 Open VSX 兼容编辑器。",
- "final.option.obsidian.body": "适合在 Obsidian 里延续同一套 HearthCode 方向。",
+ "final.subtitle": "选择你工作的地方。所有安装入口并列呈现,同时准确说明实际主题覆盖范围。",
"footer.docs": "文档",
"footer.changelog": "更新日志",
"footer.issues": "问题反馈",
diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro
index fc51508..45ad19e 100644
--- a/src/layouts/Layout.astro
+++ b/src/layouts/Layout.astro
@@ -34,6 +34,7 @@ const forge = lang === "en" ? "/forge" : `/${lang}/forge`;
const installHref = `${home}#get-started`;
const showDocs = false;
const marketplaceUrl = productData.links.marketplaceUrl;
+const zedUrl = productData.links.zedUrl;
const obsidianReleaseUrl = productData.links.obsidianUrl;
const changelogUrl = productData.links.changelogUrl;
const issuesUrl = productData.links.issuesUrl;
@@ -65,6 +66,7 @@ const ogImage = new URL(productData.site.ogImagePath, siteBase).href;
+
diff --git a/terminal/README.md b/terminal/README.md
index a2bf30f..07ff9b0 100644
--- a/terminal/README.md
+++ b/terminal/README.md
@@ -1,9 +1,11 @@
# HearthCode Terminal Themes
-HearthCode ships Moss and Ember in Dark and Light variants for Warp, Windows Terminal, Kitty, Alacritty, and iTerm2. `HearthCode Moss Dark` is the recommended starting point: it carries the clearest HearthCode identity and the strongest dark-surface separation.
+The same semantic palette across five terminal formats. HearthCode ships Moss and Ember in Dark and Light for Warp, Windows Terminal, Kitty, Alacritty, and iTerm2, so changing shells does not change what the colors mean.
All theme files in the platform directories are generated from the same terminal token contract as the editor themes. Do not edit them directly; change the color-system sources and run `pnpm run sync`.
+
+
## Warp
Copy a file from [`warp/`](./warp/) to `~/.warp/themes/`, restart Warp, then select it under **Settings → Appearance → Current Theme**.
diff --git a/terminal/hearthcode-terminal.png b/terminal/hearthcode-terminal.png
new file mode 100644
index 0000000..a2dfb89
Binary files /dev/null and b/terminal/hearthcode-terminal.png differ
diff --git a/tests/ember-role-separation.test.mjs b/tests/ember-role-separation.test.mjs
new file mode 100644
index 0000000..1ed8a2d
--- /dev/null
+++ b/tests/ember-role-separation.test.mjs
@@ -0,0 +1,43 @@
+import assert from 'node:assert/strict'
+import { readFileSync } from 'node:fs'
+import test from 'node:test'
+import { deltaE, hexHue, hueDistance, normalizeHex } from '../scripts/color-utils.mjs'
+
+function readJson(path) {
+ return JSON.parse(readFileSync(path, 'utf8'))
+}
+
+function tokenRoleColor(theme, scope) {
+ for (const entry of theme.tokenColors || []) {
+ const scopes = Array.isArray(entry.scope) ? entry.scope : [entry.scope]
+ if (!scopes.includes(scope)) continue
+ const color = typeof entry.settings === 'string' ? entry.settings : entry.settings?.foreground
+ if (normalizeHex(color)) return normalizeHex(color)
+ }
+ return null
+}
+
+test('Ember properties stay olive while strings occupy a distinct warm literal lane', () => {
+ for (const variant of ['dark', 'light']) {
+ const theme = readJson(`themes/ember-${variant}.json`)
+ const property = normalizeHex(
+ typeof theme.semanticTokenColors?.property === 'string'
+ ? theme.semanticTokenColors.property
+ : theme.semanticTokenColors?.property?.foreground,
+ )
+ const string = tokenRoleColor(theme, 'string')
+
+ assert.ok(property, `Ember ${variant} property color exists`)
+ assert.ok(string, `Ember ${variant} string color exists`)
+
+ const propertyHue = hexHue(property)
+ const stringHue = hexHue(string)
+ const roleHueDistance = hueDistance(propertyHue, stringHue)
+ const roleDeltaE = deltaE(property, string)
+
+ assert.ok(propertyHue >= 60 && propertyHue <= 90, `Ember ${variant} property hue ${propertyHue.toFixed(1)} must stay olive`)
+ assert.ok(stringHue >= 28 && stringHue <= 45, `Ember ${variant} string hue ${stringHue.toFixed(1)} must occupy the warm literal lane`)
+ assert.ok(roleHueDistance >= 30, `Ember ${variant} property/string hue distance ${roleHueDistance.toFixed(1)} < 30`)
+ assert.ok(roleDeltaE >= 24, `Ember ${variant} property/string deltaE ${roleDeltaE.toFixed(1)} < 24`)
+ }
+})
diff --git a/tests/marketing-system.test.mjs b/tests/marketing-system.test.mjs
new file mode 100644
index 0000000..09c9296
--- /dev/null
+++ b/tests/marketing-system.test.mjs
@@ -0,0 +1,398 @@
+import assert from 'node:assert/strict'
+import { createHash } from 'node:crypto'
+import { existsSync, readFileSync } from 'node:fs'
+import test from 'node:test'
+import sharp from 'sharp'
+import { loadColorProductManifest, loadColorProductPreviewConfig } from '../scripts/color-system.mjs'
+import { loadMarketingAssetSpec } from '../scripts/marketing/asset-spec.mjs'
+import { assertSemanticRiftLayout, buildSemanticRiftLayout, buildTornPaperGeometry, renderDistressedText, renderMaterialTexture, renderTornPaperSeam } from '../scripts/marketing/template-components.mjs'
+import { buildObsidianHeroSvg, buildObsidianScreenshotSvg } from '../scripts/render-obsidian-screenshot.mjs'
+
+const EXPECTED_MASTER_ASSETS = [
+ 'extension/images/editor-moss-dark-light.png',
+ 'extension/images/family-overview.png',
+ 'extension/images/theme-forge-workflow.png',
+ 'public/previews/family-overview.png',
+ 'docs/marketing/direction-atlas.png',
+ 'docs/marketing/platform-coverage.png',
+ 'docs/marketing/moss-surfaces.png',
+ 'docs/marketing/exports/github-social.png',
+ 'docs/marketing/exports/family-square.png',
+ 'docs/marketing/exports/family-portrait.png',
+ 'docs/marketing/exports/family-story.png',
+ 'docs/marketing/exports/ember-square.png',
+ 'docs/marketing/exports/moss-square.png',
+ 'zed/images/hearthcode-zed.png',
+ 'terminal/hearthcode-terminal.png',
+ 'docs/marketing/obsidian-hero.png',
+ 'obsidian/app-theme/screenshot.png',
+ 'public/og-hearth.png',
+]
+
+test('material texture primitives are deterministic and palette-injected', () => {
+ const textureArgs = {
+ id: 'test-paper',
+ ink: '#d3c9b8',
+ width: 320,
+ height: 180,
+ seed: 17,
+ intensity: 0.8,
+ }
+ const firstTexture = renderMaterialTexture(textureArgs)
+ const secondTexture = renderMaterialTexture(textureArgs)
+ const distressed = renderDistressedText({
+ id: 'test-title',
+ text: 'EMBER',
+ x: 20,
+ y: 40,
+ fill: '#d15b41',
+ wear: '#211d1a',
+ fontSize: 96,
+ seed: 23,
+ })
+
+ assert.equal(firstTexture, secondTexture)
+ assert.match(firstTexture, /patternUnits="userSpaceOnUse"/)
+ assert.match(firstTexture, /#d3c9b8/)
+ assert.match(distressed, />EMBER<\/text>/)
+ assert.match(distressed, /#d15b41/)
+ assert.match(distressed, /#211d1a/)
+})
+
+test('torn paper geometry and fibers are deterministic and palette-injected', () => {
+ const geometryArgs = {
+ controlPoints: [{ x: 280, y: 0 }, { x: 190, y: 180 }, { x: 120, y: 360 }],
+ seed: 31,
+ paperWidth: 22,
+ }
+ const firstGeometry = buildTornPaperGeometry(geometryArgs)
+ const secondGeometry = buildTornPaperGeometry(geometryArgs)
+ const firstSeam = renderTornPaperSeam({
+ id: 'test-rift',
+ geometry: firstGeometry,
+ paper: '#d3c9b8',
+ warmInk: '#ca5b41',
+ coolInk: '#93ce75',
+ shadowInk: '#1f1a17',
+ seed: 37,
+ })
+ const secondSeam = renderTornPaperSeam({
+ id: 'test-rift',
+ geometry: secondGeometry,
+ paper: '#d3c9b8',
+ warmInk: '#ca5b41',
+ coolInk: '#93ce75',
+ shadowInk: '#1f1a17',
+ seed: 37,
+ })
+
+ assert.deepEqual(firstGeometry, secondGeometry)
+ assert.equal(firstSeam, secondSeam)
+ assert.doesNotMatch(firstSeam, /feGaussianBlur/)
+ assert.match(firstSeam, /test-rift-abrasion/)
+ assert.match(firstSeam, /#d3c9b8/)
+ assert.match(firstSeam, /#ca5b41/)
+ assert.match(firstSeam, /#93ce75/)
+ assert.match(firstSeam, /#1f1a17/)
+})
+
+test('wide rift keeps scheme labels on their own fields, aligns Moss with its proof column, and makes code prominent', () => {
+ const firstLayout = buildSemanticRiftLayout({ width: 1600, height: 900 })
+ const secondLayout = buildSemanticRiftLayout({ width: 1600, height: 900 })
+
+ assert.deepEqual(firstLayout, secondLayout)
+ assert.doesNotThrow(() => assertSemanticRiftLayout(firstLayout))
+ assert.equal(firstLayout.title.mossX, firstLayout.sample.rightX)
+ assert.ok(firstLayout.sample.fontSize >= 30)
+})
+
+test('family campaign titles use the field split instead of a slash glyph', () => {
+ const generatorSource = readFileSync('scripts/generate-preview-images.mjs', 'utf8')
+
+ assert.doesNotMatch(generatorSource, /rift-title-slash/)
+ assert.doesNotMatch(generatorSource, /family-lockup-slash/)
+ assert.doesNotMatch(generatorSource, /text: "EMBER\/"/)
+})
+
+test('platform proofs keep real channel boundaries while Obsidian stays Moss-only and product-first', () => {
+ const generatorSource = readFileSync('scripts/generate-preview-images.mjs', 'utf8')
+ const themeCss = readFileSync('obsidian/app-theme/theme.css', 'utf8')
+ const obsidianScreenshot = buildObsidianScreenshotSvg(themeCss)
+ const obsidianHero = buildObsidianHeroSvg(themeCss)
+ const obsidianReadme = readFileSync('obsidian/mirror-README.md', 'utf8')
+
+ assert.match(generatorSource, /GENERATED ZED SPECIMEN · 4 THEMES/)
+ assert.match(generatorSource, /HEARTHCODE TERMINAL/)
+ assert.match(generatorSource, /5 FORMATS · 4 THEMES/)
+ assert.match(generatorSource, /buildZedSampleLines/)
+ assert.match(generatorSource, /text: `"\$\{meta\.schemeId\}"`/)
+ assert.doesNotMatch(generatorSource, /kicker: channelKicker/)
+ assert.doesNotMatch(generatorSource, /subheadline: channelSubheading/)
+ assert.doesNotMatch(obsidianScreenshot, /moss-rift/)
+ assert.doesNotMatch(obsidianHero, /moss-rift/)
+ assert.match(obsidianScreenshot, />DARK<\/text>/)
+ assert.match(obsidianScreenshot, /obsidian-community-mode-cut/)
+ assert.match(obsidianHero, /obsidian-hero-mode-cut/)
+ assert.match(obsidianHero, /clipPath/)
+ assert.match(obsidianHero, /width="1552" height="852"/)
+ assert.doesNotMatch(obsidianHero, /obsidian-dark-card/)
+ assert.doesNotMatch(obsidianHero, /obsidian-light-card/)
+ assert.doesNotMatch(obsidianHero, />MOSS<\/text>/)
+ assert.doesNotMatch(obsidianHero, />OBSIDIAN<\/text>/)
+ assert.match(obsidianHero, />DARK<\/text>/)
+ assert.match(obsidianHero, />LIGHT<\/text>/)
+ assert.doesNotMatch(obsidianHero, /EMBER/)
+ assert.match(obsidianReadme, /Moss turns color into reading order/)
+})
+
+test('attraction and product-proof templates do not inherit the field-guide wrapper', () => {
+ const generatorSource = readFileSync('scripts/generate-preview-images.mjs', 'utf8')
+ const attractionStart = generatorSource.indexOf('function renderEditorialSquareSvg')
+ const attractionEnd = generatorSource.indexOf('function renderFamilyAssetSvg')
+ const directionStart = generatorSource.indexOf('function renderDirectionCardSvg')
+ const directionEnd = generatorSource.indexOf('function renderZedUnifiedProof')
+ const channelStart = generatorSource.indexOf('function renderChannelProofSvg')
+ const channelEnd = generatorSource.indexOf('function renderAvailabilityCell')
+ const attractionTemplates = generatorSource.slice(attractionStart, attractionEnd)
+ const directionTemplate = generatorSource.slice(directionStart, directionEnd)
+ const channelTemplate = generatorSource.slice(channelStart, channelEnd)
+
+ assert.doesNotMatch(attractionTemplates, /renderFieldGuideGrid/)
+ assert.doesNotMatch(attractionTemplates, /renderRegistrationMarks/)
+ assert.doesNotMatch(directionTemplate, /renderFieldGuideGrid/)
+ assert.doesNotMatch(directionTemplate, /renderRegistrationMarks/)
+ assert.doesNotMatch(channelTemplate, /renderFieldGuideGrid/)
+ assert.doesNotMatch(channelTemplate, /renderFamilyLockup/)
+})
+
+test('marketing availability describes the actual product boundary', () => {
+ const product = loadColorProductManifest()
+ const availability = product.channelAvailability
+ const fullFamilyChannels = ['vscode', 'openvsx', 'zed', 'terminal']
+
+ for (const channelId of fullFamilyChannels) {
+ assert.deepEqual(availability[channelId].schemeIds, ['ember', 'moss'])
+ assert.deepEqual(availability[channelId].variantIds, ['dark', 'light'])
+ }
+
+ assert.deepEqual(availability.obsidian.schemeIds, ['moss'])
+ assert.deepEqual(availability.obsidian.variantIds, ['dark', 'light'])
+ assert.deepEqual(availability.vscode.capabilityIds, ['theme-forge'])
+ assert.deepEqual(availability.terminal.capabilityIds, ['five-formats'])
+ assert.deepEqual(availability.obsidian.capabilityIds, ['style-settings'])
+})
+
+test('marketing direction names Ember and Moss without promoting Amber to a theme', () => {
+ const product = loadColorProductManifest()
+ const marketingGuide = readFileSync('docs/marketing/README.md', 'utf8')
+
+ assert.deepEqual(product.supportedSchemeIds, ['moss', 'ember'])
+ assert.match(marketingGuide, /theme directions are \*\*Ember\*\* and \*\*Moss\*\*/)
+ assert.match(marketingGuide, /\*\*Amber\*\* is an Obsidian accent preset/)
+})
+
+test('preview copy carries the Semantic Materials message hierarchy', () => {
+ const preview = loadColorProductPreviewConfig()
+
+ assert.equal(preview.headline, 'Two directions. Four calibrated themes.')
+ assert.equal(preview.marketing.familyHeadline, 'EMBER / MOSS')
+ assert.equal(preview.marketing.familySubheadline, 'WARMTH OR STRUCTURE. MEANING STAYS CLEAR.')
+ assert.match(preview.marketing.directionHeadline, /Warmth or structure/)
+ assert.match(preview.marketing.platformHeadline, /Where each direction ships/)
+ assert.match(preview.marketing.mossSurfaceHeadline, /Same roles/)
+ assert.deepEqual(Object.keys(preview.marketing.directions).sort(), ['ember', 'moss'])
+ assert.equal(preview.marketing.directions.ember.sampleString, '"ember"')
+ assert.equal(preview.marketing.directions.moss.sampleString, '"moss"')
+
+ assert.equal(preview.samples.editors.language, 'typescript')
+ assert.ok(preview.samples.editors.lines.includes(' mode: "{mode}",'))
+ assert.equal(preview.samples.editors.lines.at(-1), '};')
+ assert.equal(preview.samples.obsidian.language, 'markdown')
+ assert.equal(preview.samples.terminal.lines[0], '$ pnpm run verify')
+ assert.deepEqual(preview.samples.forge.lines, [
+ 'Choose direction',
+ 'Pick seed',
+ 'Preview Dark + Light',
+ 'Apply',
+ 'Restore original theme',
+ ])
+})
+
+test('public entry copy carries one message while preserving platform truth', () => {
+ const en = JSON.parse(readFileSync('src/i18n/en.json', 'utf8'))
+ const zh = JSON.parse(readFileSync('src/i18n/zh.json', 'utf8'))
+ const ja = JSON.parse(readFileSync('src/i18n/ja.json', 'utf8'))
+ const release = JSON.parse(readFileSync('products/hearthcode/release.json', 'utf8'))
+ const rootReadme = readFileSync('README.md', 'utf8')
+ const rootReadmeZh = readFileSync('README.zh-CN.md', 'utf8')
+ const rootReadmeJa = readFileSync('README.ja.md', 'utf8')
+ const zedReadme = readFileSync('zed/mirror-README.md', 'utf8')
+ const terminalReadme = readFileSync('terminal/README.md', 'utf8')
+ const obsidianReadme = readFileSync('obsidian/mirror-README.md', 'utf8')
+
+ assert.match(en['hero.subtitle'], /^Warmth or structure\. Meaning stays clear\./)
+ assert.match(zh['hero.subtitle'], /^温暖或秩序,语义始终清晰。/)
+ assert.match(ja['hero.subtitle'], /^温もりか、構造か。意味は明瞭なまま。/)
+ assert.match(rootReadme, /\*\*Warmth or structure\. Meaning stays clear\.\*\*/)
+ assert.match(rootReadmeZh, /\*\*温暖或秩序,语义始终清晰。\*\*/)
+ assert.match(rootReadmeJa, /\*\*温もりか、構造か。意味は明瞭なまま。\*\*/)
+ assert.match(release.vscodeExtension.description, /^Warmth or structure/)
+ assert.match(release.zedExtension.description, /Warm Ember or structured Moss/)
+ assert.match(zedReadme, /paired for Zed/)
+ assert.match(terminalReadme, /same semantic palette across five terminal formats/i)
+ assert.match(obsidianReadme, /Moss turns color into reading order/)
+})
+
+function sourceSha256(path) {
+ return createHash('sha256').update(readFileSync(path)).digest('hex')
+}
+
+async function loadRgbPixelCounts(path) {
+ const { data, info } = await sharp(path).removeAlpha().raw().toBuffer({ resolveWithObject: true })
+ return (hex, channelTolerance = 0) => {
+ const normalized = hex.replace('#', '')
+ const target = [0, 2, 4].map((offset) => Number.parseInt(normalized.slice(offset, offset + 2), 16))
+ let count = 0
+ for (let index = 0; index < data.length; index += info.channels) {
+ if (
+ Math.abs(data[index] - target[0]) <= channelTolerance
+ && Math.abs(data[index + 1] - target[1]) <= channelTolerance
+ && Math.abs(data[index + 2] - target[2]) <= channelTolerance
+ ) {
+ count += 1
+ }
+ }
+ return count
+ }
+}
+
+test('preview manifest owns every canonical marketing asset', async () => {
+ const manifest = JSON.parse(readFileSync('reports/preview-manifest.json', 'utf8'))
+ const generatedOutputs = Object.values(manifest)
+ .filter((entry) => entry && typeof entry === 'object' && Array.isArray(entry.outputs))
+ .flatMap((entry) => entry.outputs)
+ const managedOutputs = (manifest.managedAssets || []).flatMap((entry) => entry.outputs || [])
+ const outputs = [...generatedOutputs, ...managedOutputs]
+
+ for (const asset of EXPECTED_MASTER_ASSETS) {
+ assert.ok(outputs.includes(asset), `${asset} must be listed in reports/preview-manifest.json`)
+ assert.ok(existsSync(asset), `${asset} must exist`)
+ }
+
+ const og = await sharp('public/og-hearth.png').metadata()
+ assert.equal(og.width, 1200)
+ assert.equal(og.height, 630)
+})
+
+test('marketing asset matrix owns templates, formats, channels, and output dimensions', async () => {
+ const spec = loadMarketingAssetSpec()
+ const manifest = JSON.parse(readFileSync('reports/preview-manifest.json', 'utf8'))
+ const specOutputs = [...spec.assets, ...spec.managedAssets].flatMap((asset) => asset.outputs).sort()
+ const manifestOutputs = [
+ ...Object.values(manifest)
+ .filter((entry) => entry && typeof entry === 'object' && Array.isArray(entry.outputs))
+ .flatMap((entry) => entry.outputs),
+ ...(manifest.managedAssets || []).flatMap((entry) => entry.outputs || []),
+ ].sort()
+
+ assert.equal(spec.brandSystem, 'semantic-materials-v1')
+ assert.equal(spec.renderer, 'semantic-materials-v1')
+ assert.deepEqual(manifestOutputs, specOutputs)
+ assert.deepEqual(spec.formats['github-social'], { width: 1280, height: 640, safeInset: 48 })
+ assert.deepEqual(spec.formats['social-square'], { width: 1200, height: 1200, safeInset: 64 })
+ assert.deepEqual(spec.formats['social-portrait'], { width: 1080, height: 1350, safeInset: 64 })
+ assert.deepEqual(spec.formats['social-story'], { width: 1080, height: 1920, safeInset: 72 })
+
+ const generatedById = Object.fromEntries(spec.assets.map((asset) => [asset.id, asset]))
+ assert.equal(generatedById['family-readme'].composition, 'semantic-rift-wide')
+ assert.equal(generatedById['github-social'].composition, 'semantic-rift-wide')
+ assert.equal(generatedById['site-og'].composition, 'semantic-rift-wide')
+ assert.equal(generatedById['family-square'].composition, 'editorial-square')
+ assert.equal(generatedById['family-portrait'].composition, 'stacked-directions')
+ assert.equal(generatedById['family-story'].composition, 'campaign-story')
+ assert.equal(generatedById['ember-square'].schemeId, 'ember')
+ assert.equal(generatedById['moss-square'].schemeId, 'moss')
+ assert.equal(generatedById['zed-platform'].channelId, 'zed')
+ assert.equal(generatedById['terminal-platform'].channelId, 'terminal')
+
+ for (const asset of [...spec.assets, ...spec.managedAssets]) {
+ for (const output of asset.outputs) {
+ assert.ok(existsSync(output), `${output} must exist`)
+ if (!asset.format) continue
+ const metadata = await sharp(output).metadata()
+ const format = spec.formats[asset.format]
+ assert.equal(metadata.width, format.width, `${output} width`)
+ assert.equal(metadata.height, format.height, `${output} height`)
+ }
+ }
+})
+
+test('marketing colors are traceable to shipped theme files and faithful PNG pixels', async () => {
+ const manifest = JSON.parse(readFileSync('reports/preview-manifest.json', 'utf8'))
+ const contract = manifest.colorFidelity
+ const countFamilyPixels = await loadRgbPixelCounts('extension/images/family-overview.png')
+ const countOgPixels = await loadRgbPixelCounts('public/og-hearth.png')
+ const responsiveFamilyCounters = await Promise.all([
+ 'docs/marketing/exports/github-social.png',
+ 'docs/marketing/exports/family-square.png',
+ 'docs/marketing/exports/family-portrait.png',
+ 'docs/marketing/exports/family-story.png',
+ ].map(loadRgbPixelCounts))
+
+ assert.equal(manifest.schemaVersion, 6)
+ assert.equal(manifest.renderer, 'semantic-materials-v1')
+ assert.equal(manifest.brandSystem.id, 'semantic-materials-v1')
+ assert.equal(manifest.brandSystem.sourceSha256, sourceSha256(manifest.brandSystem.source))
+ assert.equal(manifest.brandSystem.templateComponentsSourceSha256, sourceSha256(manifest.brandSystem.templateComponentsSource))
+ assert.equal(manifest.assetSpec.sourceSha256, sourceSha256(manifest.assetSpec.source))
+ assert.equal(contract.policy, 'theme-source-only-v1')
+ assert.deepEqual(Object.keys(contract.themes).sort(), ['ember-dark', 'ember-light', 'moss-dark', 'moss-light'])
+
+ for (const [themeId, entry] of Object.entries(contract.themes)) {
+ const source = JSON.parse(readFileSync(entry.source, 'utf8'))
+ const serializedSource = JSON.stringify(source).toLowerCase()
+
+ assert.equal(entry.sourceSha256, sourceSha256(entry.source), `${themeId} source hash must be current`)
+ assert.equal(entry.colors.surface, source.colors['editor.background'])
+ assert.equal(entry.colors.foreground, source.colors['editor.foreground'])
+ assert.ok(countFamilyPixels(entry.colors.surface) > 10_000, `${themeId} surface must appear exactly in the family PNG`)
+ assert.ok(countOgPixels(entry.colors.surface) > 10_000, `${themeId} surface must appear exactly in the OG PNG`)
+ for (const countPixels of responsiveFamilyCounters) {
+ assert.ok(countPixels(entry.colors.surface) > 10_000, `${themeId} surface must appear exactly in every responsive family export`)
+ }
+
+ for (const role of ['keyword', 'function', 'type', 'string', 'property', 'operator']) {
+ const color = entry.colors[role]
+ assert.match(color, /^#[0-9a-f]{6}$/)
+ assert.ok(serializedSource.includes(`\"${color}\"`), `${themeId} ${role} must exist verbatim in its shipped theme`)
+ // librsvg/libvips can round a source channel by one unit while writing an
+ // opaque PNG. The manifest/source equality above remains exact; this
+ // tolerance covers only that 8-bit rasterization boundary.
+ assert.ok(countFamilyPixels(color, 1) > 100, `${themeId} ${role} must appear faithfully in the family PNG`)
+ }
+ }
+})
+
+test('retired preview filenames do not return', () => {
+ for (const retired of [
+ 'extension/images/preview-contrast-v2.png',
+ 'extension/images/preview-editor-hero.png',
+ 'extension/images/preview-forge-workflow.png',
+ 'public/previews/preview-contrast-v2.png',
+ ]) {
+ assert.equal(existsSync(retired), false, `${retired} should remain retired`)
+ }
+})
+
+test('channel READMEs use their generated platform proof instead of generic artwork', () => {
+ const zedReadme = readFileSync('zed/mirror-README.md', 'utf8')
+ const terminalReadme = readFileSync('terminal/README.md', 'utf8')
+ const rootReadmes = ['README.md', 'README.zh-CN.md', 'README.ja.md'].map((path) => readFileSync(path, 'utf8'))
+
+ assert.match(zedReadme, /\.\/images\/hearthcode-zed\.png/)
+ assert.match(terminalReadme, /\.\/hearthcode-terminal\.png/)
+ for (const readme of rootReadmes) {
+ assert.match(readme, /\.\/docs\/marketing\/direction-atlas\.png/)
+ assert.match(readme, /\.\/extension\/images\/theme-forge-workflow\.png/)
+ }
+})
diff --git a/tests/theme-engine.emit-zed.test.mjs b/tests/theme-engine.emit-zed.test.mjs
new file mode 100644
index 0000000..dd95da5
--- /dev/null
+++ b/tests/theme-engine.emit-zed.test.mjs
@@ -0,0 +1,103 @@
+import { test } from 'node:test'
+import assert from 'node:assert/strict'
+import fs from 'node:fs'
+import { contrastRatio, deltaE } from '../scripts/color-utils.mjs'
+import { buildZedExtensionFiles } from '../scripts/generate-zed-themes.mjs'
+import {
+ validateZedThemeFamily,
+ ZED_STATE_DELTA_E_FLOOR,
+ ZED_THEME_SCHEMA_URL,
+} from '../scripts/theme-engine/emit/zed-core.mjs'
+
+const files = buildZedExtensionFiles()
+const themeFiles = files.filter((file) => file.path.startsWith('zed/extension/themes/'))
+
+test('Zed generator emits one manifest and four variants in two theme families', () => {
+ assert.equal(files.length, 3)
+ assert.equal(themeFiles.length, 2)
+ assert.deepEqual(
+ new Set(themeFiles.flatMap((file) => JSON.parse(file.content).themes.map((theme) => theme.name))),
+ new Set([
+ 'HearthCode Moss Dark',
+ 'HearthCode Moss Light',
+ 'HearthCode Ember Dark',
+ 'HearthCode Ember Light',
+ ]),
+ )
+})
+
+test('Zed outputs match committed generated files byte-for-byte', () => {
+ for (const file of files) {
+ assert.equal(file.content, fs.readFileSync(file.path, 'utf8'), file.path)
+ }
+})
+
+test('Zed theme families satisfy the v0.2.0 contract and preserve syntax emphasis', () => {
+ for (const file of themeFiles) {
+ const family = JSON.parse(file.content)
+ assert.equal(family.$schema, ZED_THEME_SCHEMA_URL)
+ assert.deepEqual(validateZedThemeFamily(family), [], file.path)
+ assert.deepEqual(new Set(family.themes.map((theme) => theme.appearance)), new Set(['dark', 'light']))
+ for (const theme of family.themes) {
+ assert.equal(theme.style.syntax.comment.font_style, 'italic', theme.name)
+ assert.equal(theme.style.syntax.keyword.font_weight, 700, theme.name)
+ assert.ok(theme.style.syntax.function.color, theme.name)
+ assert.ok(theme.style.syntax.property.color, theme.name)
+ assert.ok(theme.style.syntax['variable.parameter'].color, theme.name)
+ }
+ }
+})
+
+test('Zed shell and editor foreground pairs retain readable contrast', () => {
+ for (const file of themeFiles) {
+ const family = JSON.parse(file.content)
+ for (const theme of family.themes) {
+ const style = theme.style
+ const pairs = [
+ ['shell text', style.text, style.background, 4.5],
+ ['muted shell text', style['text.muted'], style.background, 4.5],
+ ['editor text', style['editor.foreground'], style['editor.background'], 4.5],
+ ['hover text', style.text, style['element.hover'], 4.5],
+ ['status text', style.text, style['status_bar.background'], 4.5],
+ ]
+ for (const [label, foreground, background, floor] of pairs) {
+ assert.ok(
+ contrastRatio(foreground, background) >= floor,
+ `${theme.name}: ${label} must remain >= ${floor}:1`,
+ )
+ }
+ }
+ }
+})
+
+test('Zed picker selection stays distinct from hover in every theme', () => {
+ for (const file of themeFiles) {
+ const family = JSON.parse(file.content)
+ for (const theme of family.themes) {
+ const style = theme.style
+ assert.ok(
+ deltaE(style['ghost_element.hover'], style['ghost_element.selected']) >= ZED_STATE_DELTA_E_FLOOR,
+ `${theme.name}: picker selected and hover fills must stay >= ${ZED_STATE_DELTA_E_FLOOR} deltaE apart`,
+ )
+ assert.ok(
+ contrastRatio(style.text, style['ghost_element.selected']) >= 4.5,
+ `${theme.name}: picker selected text must remain >= 4.5:1`,
+ )
+ }
+ }
+})
+
+test('Zed integrated terminal receives all sixteen ANSI colors', () => {
+ const ansiKeys = [
+ 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white',
+ 'bright_black', 'bright_red', 'bright_green', 'bright_yellow',
+ 'bright_blue', 'bright_magenta', 'bright_cyan', 'bright_white',
+ ]
+ for (const file of themeFiles) {
+ for (const theme of JSON.parse(file.content).themes) {
+ for (const key of ansiKeys) {
+ assert.match(theme.style[`terminal.ansi.${key}`], /^#[0-9a-f]{6}$/i, `${theme.name}: ${key}`)
+ }
+ }
+ }
+})
diff --git a/tests/theme-variant-interaction-constraints.test.mjs b/tests/theme-variant-interaction-constraints.test.mjs
index 4643798..a03dd49 100644
--- a/tests/theme-variant-interaction-constraints.test.mjs
+++ b/tests/theme-variant-interaction-constraints.test.mjs
@@ -80,7 +80,7 @@ test('generated light themes preserve the current final globalSeparation distrib
// the emitted distribution is asserted to meet the declared target (median 1.28 /
// p25 1.03 / p10 0.77) as a hard invariant on each scheme.
const expected = {
- ember: { pairCount: 291, median: '1.28', p10: '0.83', p25: '1.03', p75: '1.64' },
+ ember: { pairCount: 290, median: '1.28', p10: '0.84', p25: '1.03', p75: '1.63' },
moss: { pairCount: 290, median: '1.29', p10: '0.88', p25: '1.03', p75: '1.53' },
}
diff --git a/themes/ember-dark.json b/themes/ember-dark.json
index 7335017..9f54eeb 100644
--- a/themes/ember-dark.json
+++ b/themes/ember-dark.json
@@ -228,7 +228,7 @@
"string.regexp"
],
"settings": {
- "foreground": "#8eaa79"
+ "foreground": "#c39b69"
}
},
{
diff --git a/themes/ember-light.json b/themes/ember-light.json
index 8041e34..0f116b6 100644
--- a/themes/ember-light.json
+++ b/themes/ember-light.json
@@ -134,7 +134,7 @@
"punctuation.definition.comment"
],
"settings": {
- "foreground": "#83776e",
+ "foreground": "#83776d",
"fontStyle": "italic"
}
},
@@ -146,7 +146,7 @@
"keyword.control"
],
"settings": {
- "foreground": "#b34732",
+ "foreground": "#b6442f",
"fontStyle": "bold"
}
},
@@ -156,7 +156,7 @@
"keyword.operator.assignment"
],
"settings": {
- "foreground": "#62685c",
+ "foreground": "#64675f",
"fontStyle": ""
}
},
@@ -185,7 +185,7 @@
"meta.method-call.rust entity.name.function.rust"
],
"settings": {
- "foreground": "#a0684b"
+ "foreground": "#a46746"
}
},
{
@@ -206,7 +206,7 @@
"entity.name.function.member.rust"
],
"settings": {
- "foreground": "#a0684b"
+ "foreground": "#a46746"
}
},
{
@@ -217,7 +217,7 @@
"support.type.property-name"
],
"settings": {
- "foreground": "#555d42"
+ "foreground": "#5c6447"
}
},
{
@@ -228,7 +228,7 @@
"string.regexp"
],
"settings": {
- "foreground": "#6a7f58"
+ "foreground": "#7a4f21"
}
},
{
@@ -239,7 +239,7 @@
"constant.language.undefined"
],
"settings": {
- "foreground": "#9a637e"
+ "foreground": "#8d5e75"
}
},
{
@@ -266,7 +266,7 @@
"variable.other.constant"
],
"settings": {
- "foreground": "#49453f"
+ "foreground": "#4a453f"
}
},
{
@@ -310,7 +310,7 @@
"meta.brace"
],
"settings": {
- "foreground": "#847845"
+ "foreground": "#8c804a"
}
},
{
@@ -319,7 +319,7 @@
"support.constant"
],
"settings": {
- "foreground": "#9a637e"
+ "foreground": "#8d5e75"
}
},
{
@@ -387,14 +387,14 @@
],
"semanticHighlighting": true,
"semanticTokenColors": {
- "variable": "#49453f",
+ "variable": "#4a453f",
"variable.readonly": "#914300",
- "property": "#555d42",
+ "property": "#5c6447",
"property.readonly": "#914300",
"function": "#2e6f8f",
- "method": "#a0684b",
+ "method": "#a46746",
"function.defaultLibrary": "#2e6f8f",
- "method.defaultLibrary": "#a0684b",
+ "method.defaultLibrary": "#a46746",
"class": {
"foreground": "#736a18",
"fontStyle": "italic"
@@ -419,10 +419,10 @@
"foreground": "#736a18",
"fontStyle": "italic"
},
- "enumMember": "#9a637e",
+ "enumMember": "#8d5e75",
"namespace": "#705533",
"keyword": {
- "foreground": "#b34732",
+ "foreground": "#b6442f",
"fontStyle": "bold"
},
"parameter": "#5a4431",
diff --git a/zed/extension/extension.toml b/zed/extension/extension.toml
new file mode 100644
index 0000000..9535fd6
--- /dev/null
+++ b/zed/extension/extension.toml
@@ -0,0 +1,7 @@
+id = "hearthcode-theme"
+name = "HearthCode Theme"
+schema_version = 1
+version = "3.7.7"
+authors = ["HearthCode"]
+description = "Warm Ember or structured Moss in calibrated Dark and Light themes for Zed, with one consistent semantic color system."
+repository = "https://github.com/hearth-code/hearthcode-zed"
diff --git a/zed/extension/themes/hearthcode-ember.json b/zed/extension/themes/hearthcode-ember.json
new file mode 100644
index 0000000..98bdfed
--- /dev/null
+++ b/zed/extension/themes/hearthcode-ember.json
@@ -0,0 +1,659 @@
+{
+ "$schema": "https://zed.dev/schema/themes/v0.2.0.json",
+ "name": "HearthCode Ember",
+ "author": "HearthCode",
+ "themes": [
+ {
+ "name": "HearthCode Ember Dark",
+ "appearance": "dark",
+ "style": {
+ "background.appearance": "opaque",
+ "background": "#16120f",
+ "surface.background": "#2c241f",
+ "elevated_surface.background": "#443e38",
+ "border": "#332d29",
+ "border.variant": "#48423b",
+ "border.focused": "#9a5426",
+ "border.selected": "#dc8d46",
+ "border.transparent": "#00000000",
+ "border.disabled": "#332d29",
+ "pane_group.border": "#332d29",
+ "pane.focused_border": "#9a5426",
+ "panel.focused_border": "#9a5426",
+ "element.background": "#2f2b26",
+ "element.hover": "#3b3630",
+ "element.active": "#3b3630",
+ "element.selected": "#3b3630",
+ "element.disabled": "#2f2b26",
+ "drop_target.background": "#dc8d4640",
+ "ghost_element.background": "#00000000",
+ "ghost_element.hover": "#2f2b26",
+ "ghost_element.active": "#3e3933",
+ "ghost_element.selected": "#3e3933",
+ "ghost_element.disabled": "#00000000",
+ "text": "#d3c9b8",
+ "text.muted": "#989083",
+ "text.placeholder": "#5f5850",
+ "text.disabled": "#7d766b",
+ "text.accent": "#ca5b41",
+ "icon": "#d3c9b8",
+ "icon.muted": "#989083",
+ "icon.disabled": "#7d766b",
+ "icon.placeholder": "#5f5850",
+ "icon.accent": "#ca5b41",
+ "status_bar.background": "#2c241f",
+ "title_bar.background": "#16120f",
+ "title_bar.inactive_background": "#16120f",
+ "toolbar.background": "#2c241f",
+ "tab_bar.background": "#16120f",
+ "tab.inactive_background": "#16120f",
+ "tab.active_background": "#1f1a17",
+ "search.match_background": "#dc8d462f",
+ "panel.background": "#16120f",
+ "scrollbar.thumb.background": "#9890834d",
+ "scrollbar.thumb.hover_background": "#98908380",
+ "scrollbar.thumb.border": "#332d29b3",
+ "scrollbar.track.background": "#00000000",
+ "scrollbar.track.border": "#332d29",
+ "editor.background": "#1f1a17",
+ "editor.foreground": "#d3c9b8",
+ "editor.gutter.background": "#1f1a17",
+ "editor.subheader.background": "#292420",
+ "editor.active_line.background": "#292420",
+ "editor.highlighted_line.background": "#292420",
+ "editor.line_number": "#615a52",
+ "editor.active_line_number": "#e8ddca",
+ "editor.invisible": "#48423b",
+ "editor.wrap_guide": "#48423b66",
+ "editor.active_wrap_guide": "#655b4a",
+ "editor.indent_guide": "#48423b",
+ "editor.indent_guide_active": "#655b4a",
+ "editor.document_highlight.bracket_background": "#dc8d4618",
+ "editor.document_highlight.read_background": "#dc8d462f",
+ "editor.document_highlight.write_background": "#9a542642",
+ "terminal.background": "#1f1a17",
+ "terminal.foreground": "#d3c9b8",
+ "terminal.bright_foreground": "#e8ddca",
+ "terminal.dim_foreground": "#7d766b",
+ "terminal.ansi.background": "#1f1a17",
+ "terminal.ansi.black": "#2f2921",
+ "terminal.ansi.red": "#c66a52",
+ "terminal.ansi.green": "#87ab70",
+ "terminal.ansi.yellow": "#b89543",
+ "terminal.ansi.blue": "#6f8ca4",
+ "terminal.ansi.magenta": "#b38b6a",
+ "terminal.ansi.cyan": "#79a8a2",
+ "terminal.ansi.white": "#d3c9b8",
+ "terminal.ansi.bright_black": "#655c4f",
+ "terminal.ansi.bright_red": "#d37962",
+ "terminal.ansi.bright_green": "#96b986",
+ "terminal.ansi.bright_yellow": "#c7a95b",
+ "terminal.ansi.bright_blue": "#86a4bc",
+ "terminal.ansi.bright_magenta": "#c5a080",
+ "terminal.ansi.bright_cyan": "#8cb9b3",
+ "terminal.ansi.bright_white": "#e8ddca",
+ "accents": [
+ "#dc8d46",
+ "#9a5426",
+ "#e1ba73",
+ "#8dbb75",
+ "#6f94a4",
+ "#98ab7d",
+ "#cb844a",
+ "#cc5a3f"
+ ],
+ "players": [
+ {
+ "cursor": "#dc8d46",
+ "background": "#dc8d46",
+ "selection": "#dc8d463d"
+ },
+ {
+ "cursor": "#9a5426",
+ "background": "#9a5426",
+ "selection": "#9a54263d"
+ },
+ {
+ "cursor": "#e1ba73",
+ "background": "#e1ba73",
+ "selection": "#e1ba733d"
+ },
+ {
+ "cursor": "#8dbb75",
+ "background": "#8dbb75",
+ "selection": "#8dbb753d"
+ },
+ {
+ "cursor": "#6f94a4",
+ "background": "#6f94a4",
+ "selection": "#6f94a43d"
+ },
+ {
+ "cursor": "#98ab7d",
+ "background": "#98ab7d",
+ "selection": "#98ab7d3d"
+ },
+ {
+ "cursor": "#cb844a",
+ "background": "#cb844a",
+ "selection": "#cb844a3d"
+ },
+ {
+ "cursor": "#cc5a3f",
+ "background": "#cc5a3f",
+ "selection": "#cc5a3f3d"
+ }
+ ],
+ "syntax": {
+ "comment": {
+ "color": "#756958",
+ "font_style": "italic"
+ },
+ "comment.doc": {
+ "color": "#756958",
+ "font_style": "italic"
+ },
+ "keyword": {
+ "color": "#ca5b41",
+ "font_weight": 700
+ },
+ "preproc": {
+ "color": "#ca5b41",
+ "font_weight": 700
+ },
+ "operator": {
+ "color": "#9c978f"
+ },
+ "punctuation": {
+ "color": "#a29663"
+ },
+ "punctuation.bracket": {
+ "color": "#a29663"
+ },
+ "punctuation.delimiter": {
+ "color": "#a29663"
+ },
+ "punctuation.list_marker": {
+ "color": "#a29663"
+ },
+ "punctuation.special": {
+ "color": "#a29663"
+ },
+ "function": {
+ "color": "#6f94a4"
+ },
+ "constructor": {
+ "color": "#6f94a4"
+ },
+ "property": {
+ "color": "#788058"
+ },
+ "attribute": {
+ "color": "#788058"
+ },
+ "label": {
+ "color": "#788058"
+ },
+ "string": {
+ "color": "#c39b69"
+ },
+ "string.escape": {
+ "color": "#c39b69"
+ },
+ "string.regex": {
+ "color": "#c39b69"
+ },
+ "string.special": {
+ "color": "#c39b69"
+ },
+ "string.special.symbol": {
+ "color": "#c39b69"
+ },
+ "number": {
+ "color": "#aa7a94"
+ },
+ "constant": {
+ "color": "#aa7a94"
+ },
+ "constant.builtin": {
+ "color": "#aa7a94"
+ },
+ "boolean": {
+ "color": "#aa7a94"
+ },
+ "enum": {
+ "color": "#aa7a94"
+ },
+ "variant": {
+ "color": "#aa7a94"
+ },
+ "type": {
+ "color": "#99904c",
+ "font_style": "italic"
+ },
+ "type.builtin": {
+ "color": "#99904c",
+ "font_style": "italic"
+ },
+ "variable": {
+ "color": "#c3bfb9"
+ },
+ "variable.special": {
+ "color": "#c3bfb9"
+ },
+ "variable.parameter": {
+ "color": "#c0b5a3"
+ },
+ "tag": {
+ "color": "#af513b"
+ },
+ "tag.doctype": {
+ "color": "#af513b"
+ },
+ "primary": {
+ "color": "#d3c9b8"
+ },
+ "embedded": {
+ "color": "#d3c9b8"
+ },
+ "hint": {
+ "color": "#788058"
+ },
+ "predictive": {
+ "color": "#788058"
+ },
+ "link_text": {
+ "color": "#6f94a4"
+ },
+ "link_uri": {
+ "color": "#6f94a4",
+ "font_style": "italic"
+ },
+ "emphasis": {
+ "color": "#d3c9b8",
+ "font_style": "italic"
+ },
+ "emphasis.strong": {
+ "color": "#d3c9b8",
+ "font_weight": 700
+ },
+ "title": {
+ "color": "#ca5b41",
+ "font_weight": 700
+ },
+ "text.literal": {
+ "color": "#c39b69"
+ }
+ },
+ "hint": "#788058",
+ "hint.background": "#7880581a",
+ "hint.border": "#78805866",
+ "hidden": "#788058",
+ "hidden.background": "#7880581a",
+ "hidden.border": "#78805866",
+ "ignored": "#788058",
+ "ignored.background": "#7880581a",
+ "ignored.border": "#78805866",
+ "predictive": "#788058",
+ "predictive.background": "#7880581a",
+ "predictive.border": "#78805866",
+ "unreachable": "#788058",
+ "unreachable.background": "#7880581a",
+ "unreachable.border": "#78805866",
+ "info": "#6f94a4",
+ "info.background": "#6f94a41a",
+ "info.border": "#6f94a466",
+ "renamed": "#6f94a4",
+ "renamed.background": "#6f94a41a",
+ "renamed.border": "#6f94a466",
+ "success": "#98ab7d",
+ "success.background": "#98ab7d1a",
+ "success.border": "#98ab7d66",
+ "created": "#98ab7d",
+ "created.background": "#98ab7d1a",
+ "created.border": "#98ab7d66",
+ "warning": "#cb844a",
+ "warning.background": "#cb844a1a",
+ "warning.border": "#cb844a66",
+ "modified": "#cb844a",
+ "modified.background": "#cb844a1a",
+ "modified.border": "#cb844a66",
+ "conflict": "#cb844a",
+ "conflict.background": "#cb844a1a",
+ "conflict.border": "#cb844a66",
+ "error": "#cc5a3f",
+ "error.background": "#cc5a3f1a",
+ "error.border": "#cc5a3f66",
+ "deleted": "#cc5a3f",
+ "deleted.background": "#cc5a3f1a",
+ "deleted.border": "#cc5a3f66"
+ }
+ },
+ {
+ "name": "HearthCode Ember Light",
+ "appearance": "light",
+ "style": {
+ "background.appearance": "opaque",
+ "background": "#e1d6c7",
+ "surface.background": "#e5dacd",
+ "elevated_surface.background": "#d3c8b9",
+ "border": "#d0c4b3",
+ "border.variant": "#d5c9b9",
+ "border.focused": "#a85120",
+ "border.selected": "#4c7688",
+ "border.transparent": "#00000000",
+ "border.disabled": "#d0c4b3",
+ "pane_group.border": "#d0c4b3",
+ "pane.focused_border": "#a85120",
+ "panel.focused_border": "#a85120",
+ "element.background": "#dbd0c1",
+ "element.hover": "#d9cec0",
+ "element.active": "#d9cec0",
+ "element.selected": "#d9cec0",
+ "element.disabled": "#dbd0c1",
+ "drop_target.background": "#4c768840",
+ "ghost_element.background": "#00000000",
+ "ghost_element.hover": "#dbd0c1",
+ "ghost_element.active": "#c9beb0",
+ "ghost_element.selected": "#c9beb0",
+ "ghost_element.disabled": "#00000000",
+ "text": "#352b21",
+ "text.muted": "#5a5045",
+ "text.placeholder": "#a99f91",
+ "text.disabled": "#80766a",
+ "text.accent": "#b6442f",
+ "icon": "#352b21",
+ "icon.muted": "#5a5045",
+ "icon.disabled": "#80766a",
+ "icon.placeholder": "#a99f91",
+ "icon.accent": "#b6442f",
+ "status_bar.background": "#e5dacd",
+ "title_bar.background": "#e1d6c7",
+ "title_bar.inactive_background": "#e1d6c7",
+ "toolbar.background": "#e5dacd",
+ "tab_bar.background": "#e1d6c7",
+ "tab.inactive_background": "#e1d6c7",
+ "tab.active_background": "#ecdfcd",
+ "search.match_background": "#4c768820",
+ "panel.background": "#e1d6c7",
+ "scrollbar.thumb.background": "#5a50454d",
+ "scrollbar.thumb.hover_background": "#5a504580",
+ "scrollbar.thumb.border": "#d0c4b3b3",
+ "scrollbar.track.background": "#00000000",
+ "scrollbar.track.border": "#d0c4b3",
+ "editor.background": "#ecdfcd",
+ "editor.foreground": "#30261b",
+ "editor.gutter.background": "#ecdfcd",
+ "editor.subheader.background": "#dccfbe",
+ "editor.active_line.background": "#dccfbe",
+ "editor.highlighted_line.background": "#dccfbe",
+ "editor.line_number": "#b2a696",
+ "editor.active_line_number": "#352b21",
+ "editor.invisible": "#d5c9b9",
+ "editor.wrap_guide": "#d5c9b966",
+ "editor.active_wrap_guide": "#b49d7b",
+ "editor.indent_guide": "#d5c9b9",
+ "editor.indent_guide_active": "#b49d7b",
+ "editor.document_highlight.bracket_background": "#4c768816",
+ "editor.document_highlight.read_background": "#4c768820",
+ "editor.document_highlight.write_background": "#a8512042",
+ "terminal.background": "#ecdfcd",
+ "terminal.foreground": "#30261b",
+ "terminal.bright_foreground": "#2f210e",
+ "terminal.dim_foreground": "#80766a",
+ "terminal.ansi.background": "#ecdfcd",
+ "terminal.ansi.black": "#3a3020",
+ "terminal.ansi.red": "#ad3518",
+ "terminal.ansi.green": "#2e6a2c",
+ "terminal.ansi.yellow": "#7f6531",
+ "terminal.ansi.blue": "#2a5070",
+ "terminal.ansi.magenta": "#784418",
+ "terminal.ansi.cyan": "#2a6050",
+ "terminal.ansi.white": "#624a2c",
+ "terminal.ansi.bright_black": "#7f6d53",
+ "terminal.ansi.bright_red": "#be4a2a",
+ "terminal.ansi.bright_green": "#3b7f38",
+ "terminal.ansi.bright_yellow": "#927a3e",
+ "terminal.ansi.bright_blue": "#3a6080",
+ "terminal.ansi.bright_magenta": "#885428",
+ "terminal.ansi.bright_cyan": "#3a7060",
+ "terminal.ansi.bright_white": "#2f210e",
+ "accents": [
+ "#4c7688",
+ "#a85120",
+ "#674100",
+ "#2f6f2d",
+ "#416d86",
+ "#6a7d58",
+ "#ba7239",
+ "#b04935"
+ ],
+ "players": [
+ {
+ "cursor": "#4c7688",
+ "background": "#4c7688",
+ "selection": "#4c76883d"
+ },
+ {
+ "cursor": "#a85120",
+ "background": "#a85120",
+ "selection": "#a851203d"
+ },
+ {
+ "cursor": "#674100",
+ "background": "#674100",
+ "selection": "#6741003d"
+ },
+ {
+ "cursor": "#2f6f2d",
+ "background": "#2f6f2d",
+ "selection": "#2f6f2d3d"
+ },
+ {
+ "cursor": "#416d86",
+ "background": "#416d86",
+ "selection": "#416d863d"
+ },
+ {
+ "cursor": "#6a7d58",
+ "background": "#6a7d58",
+ "selection": "#6a7d583d"
+ },
+ {
+ "cursor": "#ba7239",
+ "background": "#ba7239",
+ "selection": "#ba72393d"
+ },
+ {
+ "cursor": "#b04935",
+ "background": "#b04935",
+ "selection": "#b049353d"
+ }
+ ],
+ "syntax": {
+ "comment": {
+ "color": "#83776d",
+ "font_style": "italic"
+ },
+ "comment.doc": {
+ "color": "#83776d",
+ "font_style": "italic"
+ },
+ "keyword": {
+ "color": "#b6442f",
+ "font_weight": 700
+ },
+ "preproc": {
+ "color": "#b6442f",
+ "font_weight": 700
+ },
+ "operator": {
+ "color": "#64675f"
+ },
+ "punctuation": {
+ "color": "#8c804a"
+ },
+ "punctuation.bracket": {
+ "color": "#8c804a"
+ },
+ "punctuation.delimiter": {
+ "color": "#8c804a"
+ },
+ "punctuation.list_marker": {
+ "color": "#8c804a"
+ },
+ "punctuation.special": {
+ "color": "#8c804a"
+ },
+ "function": {
+ "color": "#2e6f8f"
+ },
+ "constructor": {
+ "color": "#2e6f8f"
+ },
+ "property": {
+ "color": "#5c6447"
+ },
+ "attribute": {
+ "color": "#5c6447"
+ },
+ "label": {
+ "color": "#5c6447"
+ },
+ "string": {
+ "color": "#7a4f21"
+ },
+ "string.escape": {
+ "color": "#7a4f21"
+ },
+ "string.regex": {
+ "color": "#7a4f21"
+ },
+ "string.special": {
+ "color": "#7a4f21"
+ },
+ "string.special.symbol": {
+ "color": "#7a4f21"
+ },
+ "number": {
+ "color": "#8d5e75"
+ },
+ "constant": {
+ "color": "#8d5e75"
+ },
+ "constant.builtin": {
+ "color": "#8d5e75"
+ },
+ "boolean": {
+ "color": "#8d5e75"
+ },
+ "enum": {
+ "color": "#8d5e75"
+ },
+ "variant": {
+ "color": "#8d5e75"
+ },
+ "type": {
+ "color": "#736a18",
+ "font_style": "italic"
+ },
+ "type.builtin": {
+ "color": "#736a18",
+ "font_style": "italic"
+ },
+ "variable": {
+ "color": "#4a453f"
+ },
+ "variable.special": {
+ "color": "#4a453f"
+ },
+ "variable.parameter": {
+ "color": "#5a4431"
+ },
+ "tag": {
+ "color": "#77291e"
+ },
+ "tag.doctype": {
+ "color": "#77291e"
+ },
+ "primary": {
+ "color": "#30261b"
+ },
+ "embedded": {
+ "color": "#30261b"
+ },
+ "hint": {
+ "color": "#5c6448"
+ },
+ "predictive": {
+ "color": "#5c6448"
+ },
+ "link_text": {
+ "color": "#416d86"
+ },
+ "link_uri": {
+ "color": "#416d86",
+ "font_style": "italic"
+ },
+ "emphasis": {
+ "color": "#30261b",
+ "font_style": "italic"
+ },
+ "emphasis.strong": {
+ "color": "#30261b",
+ "font_weight": 700
+ },
+ "title": {
+ "color": "#b6442f",
+ "font_weight": 700
+ },
+ "text.literal": {
+ "color": "#7a4f21"
+ }
+ },
+ "hint": "#5c6448",
+ "hint.background": "#5c64481a",
+ "hint.border": "#5c644866",
+ "hidden": "#5c6448",
+ "hidden.background": "#5c64481a",
+ "hidden.border": "#5c644866",
+ "ignored": "#5c6448",
+ "ignored.background": "#5c64481a",
+ "ignored.border": "#5c644866",
+ "predictive": "#5c6448",
+ "predictive.background": "#5c64481a",
+ "predictive.border": "#5c644866",
+ "unreachable": "#5c6448",
+ "unreachable.background": "#5c64481a",
+ "unreachable.border": "#5c644866",
+ "info": "#416d86",
+ "info.background": "#416d861a",
+ "info.border": "#416d8666",
+ "renamed": "#416d86",
+ "renamed.background": "#416d861a",
+ "renamed.border": "#416d8666",
+ "success": "#6a7d58",
+ "success.background": "#6a7d581a",
+ "success.border": "#6a7d5866",
+ "created": "#6a7d58",
+ "created.background": "#6a7d581a",
+ "created.border": "#6a7d5866",
+ "warning": "#ba7239",
+ "warning.background": "#ba72391a",
+ "warning.border": "#ba723966",
+ "modified": "#ba7239",
+ "modified.background": "#ba72391a",
+ "modified.border": "#ba723966",
+ "conflict": "#ba7239",
+ "conflict.background": "#ba72391a",
+ "conflict.border": "#ba723966",
+ "error": "#b04935",
+ "error.background": "#b049351a",
+ "error.border": "#b0493566",
+ "deleted": "#b04935",
+ "deleted.background": "#b049351a",
+ "deleted.border": "#b0493566"
+ }
+ }
+ ]
+}
diff --git a/zed/extension/themes/hearthcode-moss.json b/zed/extension/themes/hearthcode-moss.json
new file mode 100644
index 0000000..d4f76c3
--- /dev/null
+++ b/zed/extension/themes/hearthcode-moss.json
@@ -0,0 +1,641 @@
+{
+ "$schema": "https://zed.dev/schema/themes/v0.2.0.json",
+ "name": "HearthCode Moss",
+ "author": "HearthCode",
+ "themes": [
+ {
+ "name": "HearthCode Moss Dark",
+ "appearance": "dark",
+ "style": {
+ "background.appearance": "opaque",
+ "background": "#191815",
+ "surface.background": "#282521",
+ "elevated_surface.background": "#443e35",
+ "border": "#3c3a32",
+ "border.variant": "#454138",
+ "border.focused": "#b37f16",
+ "border.selected": "#8bb49e",
+ "border.transparent": "#00000000",
+ "border.disabled": "#3c3a32",
+ "pane_group.border": "#3c3a32",
+ "pane.focused_border": "#b37f16",
+ "panel.focused_border": "#b37f16",
+ "element.background": "#2d2a25",
+ "element.hover": "#3a362e",
+ "element.active": "#3a362e",
+ "element.selected": "#3a362e",
+ "element.disabled": "#2d2a25",
+ "drop_target.background": "#8bb49e40",
+ "ghost_element.background": "#00000000",
+ "ghost_element.hover": "#2d2a25",
+ "ghost_element.active": "#3a362e",
+ "ghost_element.selected": "#3a362e",
+ "ghost_element.disabled": "#00000000",
+ "text": "#d2bea2",
+ "text.muted": "#a99983",
+ "text.placeholder": "#685f52",
+ "text.disabled": "#847867",
+ "text.accent": "#d4b84f",
+ "icon": "#d2bea2",
+ "icon.muted": "#a99983",
+ "icon.disabled": "#847867",
+ "icon.placeholder": "#685f52",
+ "icon.accent": "#d4b84f",
+ "status_bar.background": "#282521",
+ "title_bar.background": "#191815",
+ "title_bar.inactive_background": "#191815",
+ "toolbar.background": "#282521",
+ "tab_bar.background": "#191815",
+ "tab.inactive_background": "#191815",
+ "tab.active_background": "#1b1d1a",
+ "search.match_background": "#cb93222e",
+ "panel.background": "#191815",
+ "scrollbar.thumb.background": "#a999834d",
+ "scrollbar.thumb.hover_background": "#a9998380",
+ "scrollbar.thumb.border": "#3c3a32b3",
+ "scrollbar.track.background": "#00000000",
+ "scrollbar.track.border": "#3c3a32",
+ "editor.background": "#1b1d1a",
+ "editor.foreground": "#d2bea2",
+ "editor.gutter.background": "#1b1d1a",
+ "editor.subheader.background": "#272824",
+ "editor.active_line.background": "#272824",
+ "editor.highlighted_line.background": "#272824",
+ "editor.line_number": "#5d574b",
+ "editor.active_line_number": "#e4d5bc",
+ "editor.invisible": "#454138",
+ "editor.wrap_guide": "#45413866",
+ "editor.active_wrap_guide": "#918675",
+ "editor.indent_guide": "#454138",
+ "editor.indent_guide_active": "#918675",
+ "editor.document_highlight.bracket_background": "#d4b84f1a",
+ "editor.document_highlight.read_background": "#cb93222e",
+ "editor.document_highlight.write_background": "#b37f1642",
+ "terminal.background": "#1b1d1a",
+ "terminal.foreground": "#d2bea2",
+ "terminal.bright_foreground": "#f1e6d4",
+ "terminal.dim_foreground": "#847867",
+ "terminal.ansi.background": "#1b1d1a",
+ "terminal.ansi.black": "#1d201f",
+ "terminal.ansi.red": "#e06c5e",
+ "terminal.ansi.green": "#9fb86f",
+ "terminal.ansi.yellow": "#d2ab61",
+ "terminal.ansi.blue": "#79a89f",
+ "terminal.ansi.magenta": "#ca8ea5",
+ "terminal.ansi.cyan": "#8fb686",
+ "terminal.ansi.white": "#d6c3a3",
+ "terminal.ansi.bright_black": "#675d52",
+ "terminal.ansi.bright_red": "#ee8376",
+ "terminal.ansi.bright_green": "#b6c97f",
+ "terminal.ansi.bright_yellow": "#deba72",
+ "terminal.ansi.bright_blue": "#92bbb1",
+ "terminal.ansi.bright_magenta": "#dc9fb2",
+ "terminal.ansi.bright_cyan": "#9bc099",
+ "terminal.ansi.bright_white": "#f1e6d4",
+ "accents": [
+ "#8bb49e",
+ "#b37f16",
+ "#d4b84f",
+ "#d2a33d",
+ "#68adc1",
+ "#91cf72"
+ ],
+ "players": [
+ {
+ "cursor": "#8bb49e",
+ "background": "#8bb49e",
+ "selection": "#8bb49e3d"
+ },
+ {
+ "cursor": "#b37f16",
+ "background": "#b37f16",
+ "selection": "#b37f163d"
+ },
+ {
+ "cursor": "#d4b84f",
+ "background": "#d4b84f",
+ "selection": "#d4b84f3d"
+ },
+ {
+ "cursor": "#d2a33d",
+ "background": "#d2a33d",
+ "selection": "#d2a33d3d"
+ },
+ {
+ "cursor": "#68adc1",
+ "background": "#68adc1",
+ "selection": "#68adc13d"
+ },
+ {
+ "cursor": "#91cf72",
+ "background": "#91cf72",
+ "selection": "#91cf723d"
+ }
+ ],
+ "syntax": {
+ "comment": {
+ "color": "#817566",
+ "font_style": "italic"
+ },
+ "comment.doc": {
+ "color": "#817566",
+ "font_style": "italic"
+ },
+ "keyword": {
+ "color": "#d4b84f",
+ "font_weight": 700
+ },
+ "preproc": {
+ "color": "#d4b84f",
+ "font_weight": 700
+ },
+ "operator": {
+ "color": "#979087"
+ },
+ "punctuation": {
+ "color": "#b47a8b"
+ },
+ "punctuation.bracket": {
+ "color": "#b47a8b"
+ },
+ "punctuation.delimiter": {
+ "color": "#b47a8b"
+ },
+ "punctuation.list_marker": {
+ "color": "#b47a8b"
+ },
+ "punctuation.special": {
+ "color": "#b47a8b"
+ },
+ "function": {
+ "color": "#93ce75"
+ },
+ "constructor": {
+ "color": "#93ce75"
+ },
+ "property": {
+ "color": "#8ebb7f"
+ },
+ "attribute": {
+ "color": "#8ebb7f"
+ },
+ "label": {
+ "color": "#8ebb7f"
+ },
+ "string": {
+ "color": "#caa46c"
+ },
+ "string.escape": {
+ "color": "#caa46c"
+ },
+ "string.regex": {
+ "color": "#caa46c"
+ },
+ "string.special": {
+ "color": "#caa46c"
+ },
+ "string.special.symbol": {
+ "color": "#caa46c"
+ },
+ "number": {
+ "color": "#77a7d2"
+ },
+ "constant": {
+ "color": "#77a7d2"
+ },
+ "constant.builtin": {
+ "color": "#77a7d2"
+ },
+ "boolean": {
+ "color": "#77a7d2"
+ },
+ "enum": {
+ "color": "#77a7d2"
+ },
+ "variant": {
+ "color": "#77a7d2"
+ },
+ "type": {
+ "color": "#68adc1",
+ "font_style": "italic"
+ },
+ "type.builtin": {
+ "color": "#68adc1",
+ "font_style": "italic"
+ },
+ "variable": {
+ "color": "#c3bfb9"
+ },
+ "variable.special": {
+ "color": "#c3bfb9"
+ },
+ "variable.parameter": {
+ "color": "#c2b08f"
+ },
+ "tag": {
+ "color": "#c5bf56"
+ },
+ "tag.doctype": {
+ "color": "#c5bf56"
+ },
+ "primary": {
+ "color": "#d2bea2"
+ },
+ "embedded": {
+ "color": "#d2bea2"
+ },
+ "hint": {
+ "color": "#817566"
+ },
+ "predictive": {
+ "color": "#817566"
+ },
+ "link_text": {
+ "color": "#68adc1"
+ },
+ "link_uri": {
+ "color": "#68adc1",
+ "font_style": "italic"
+ },
+ "emphasis": {
+ "color": "#d2bea2",
+ "font_style": "italic"
+ },
+ "emphasis.strong": {
+ "color": "#d2bea2",
+ "font_weight": 700
+ },
+ "title": {
+ "color": "#d4b84f",
+ "font_weight": 700
+ },
+ "text.literal": {
+ "color": "#caa46c"
+ }
+ },
+ "hint": "#817566",
+ "hint.background": "#8175661a",
+ "hint.border": "#81756666",
+ "hidden": "#817566",
+ "hidden.background": "#8175661a",
+ "hidden.border": "#81756666",
+ "ignored": "#817566",
+ "ignored.background": "#8175661a",
+ "ignored.border": "#81756666",
+ "predictive": "#817566",
+ "predictive.background": "#8175661a",
+ "predictive.border": "#81756666",
+ "unreachable": "#817566",
+ "unreachable.background": "#8175661a",
+ "unreachable.border": "#81756666",
+ "info": "#68adc1",
+ "info.background": "#68adc11a",
+ "info.border": "#68adc166",
+ "renamed": "#68adc1",
+ "renamed.background": "#68adc11a",
+ "renamed.border": "#68adc166",
+ "success": "#91cf72",
+ "success.background": "#91cf721a",
+ "success.border": "#91cf7266",
+ "created": "#91cf72",
+ "created.background": "#91cf721a",
+ "created.border": "#91cf7266",
+ "warning": "#d2a33d",
+ "warning.background": "#d2a33d1a",
+ "warning.border": "#d2a33d66",
+ "modified": "#d2a33d",
+ "modified.background": "#d2a33d1a",
+ "modified.border": "#d2a33d66",
+ "conflict": "#d2a33d",
+ "conflict.background": "#d2a33d1a",
+ "conflict.border": "#d2a33d66",
+ "error": "#d4b84f",
+ "error.background": "#d4b84f1a",
+ "error.border": "#d4b84f66",
+ "deleted": "#d4b84f",
+ "deleted.background": "#d4b84f1a",
+ "deleted.border": "#d4b84f66"
+ }
+ },
+ {
+ "name": "HearthCode Moss Light",
+ "appearance": "light",
+ "style": {
+ "background.appearance": "opaque",
+ "background": "#d4d1c4",
+ "surface.background": "#c7c4b8",
+ "elevated_surface.background": "#bdb9ad",
+ "border": "#c3c0b5",
+ "border.variant": "#c8c5b9",
+ "border.focused": "#bb7c12",
+ "border.selected": "#486a59",
+ "border.transparent": "#00000000",
+ "border.disabled": "#c3c0b5",
+ "pane_group.border": "#c3c0b5",
+ "pane.focused_border": "#bb7c12",
+ "panel.focused_border": "#bb7c12",
+ "element.background": "#c7c4b8",
+ "element.hover": "#bebaae",
+ "element.active": "#bebaae",
+ "element.selected": "#bebaae",
+ "element.disabled": "#c7c4b8",
+ "drop_target.background": "#486a5940",
+ "ghost_element.background": "#00000000",
+ "ghost_element.hover": "#c7c4b8",
+ "ghost_element.active": "#b8b4a9",
+ "ghost_element.selected": "#b8b4a9",
+ "ghost_element.disabled": "#00000000",
+ "text": "#342d28",
+ "text.muted": "#47413b",
+ "text.placeholder": "#7f7a71",
+ "text.disabled": "#645e57",
+ "text.accent": "#83640f",
+ "icon": "#342d28",
+ "icon.muted": "#47413b",
+ "icon.disabled": "#645e57",
+ "icon.placeholder": "#7f7a71",
+ "icon.accent": "#83640f",
+ "status_bar.background": "#c7c4b8",
+ "title_bar.background": "#d4d1c4",
+ "title_bar.inactive_background": "#d4d1c4",
+ "toolbar.background": "#c7c4b8",
+ "tab_bar.background": "#d4d1c4",
+ "tab.inactive_background": "#d4d1c4",
+ "tab.active_background": "#e7e5d8",
+ "search.match_background": "#b8791126",
+ "panel.background": "#d4d1c4",
+ "scrollbar.thumb.background": "#47413b4d",
+ "scrollbar.thumb.hover_background": "#47413b80",
+ "scrollbar.thumb.border": "#c3c0b5b3",
+ "scrollbar.track.background": "#00000000",
+ "scrollbar.track.border": "#c3c0b5",
+ "editor.background": "#e7e5d8",
+ "editor.foreground": "#342d28",
+ "editor.gutter.background": "#e7e5d8",
+ "editor.subheader.background": "#cecabe",
+ "editor.active_line.background": "#cecabe",
+ "editor.highlighted_line.background": "#cecabe",
+ "editor.line_number": "#a7a399",
+ "editor.active_line_number": "#2c2722",
+ "editor.invisible": "#c8c5b9",
+ "editor.wrap_guide": "#c8c5b966",
+ "editor.active_wrap_guide": "#8b7d63",
+ "editor.indent_guide": "#c8c5b9",
+ "editor.indent_guide_active": "#8b7d63",
+ "editor.document_highlight.bracket_background": "#83640f14",
+ "editor.document_highlight.read_background": "#b8791126",
+ "editor.document_highlight.write_background": "#bb7c1242",
+ "terminal.background": "#e7e5d8",
+ "terminal.foreground": "#342d28",
+ "terminal.bright_foreground": "#2d241e",
+ "terminal.dim_foreground": "#645e57",
+ "terminal.ansi.background": "#e7e5d8",
+ "terminal.ansi.black": "#3a342e",
+ "terminal.ansi.red": "#ba5146",
+ "terminal.ansi.green": "#718246",
+ "terminal.ansi.yellow": "#9d7124",
+ "terminal.ansi.blue": "#517d76",
+ "terminal.ansi.magenta": "#a26179",
+ "terminal.ansi.cyan": "#608763",
+ "terminal.ansi.white": "#43382e",
+ "terminal.ansi.bright_black": "#8c7c69",
+ "terminal.ansi.bright_red": "#c9675c",
+ "terminal.ansi.bright_green": "#79914c",
+ "terminal.ansi.bright_yellow": "#ab8034",
+ "terminal.ansi.bright_blue": "#658d85",
+ "terminal.ansi.bright_magenta": "#b9758b",
+ "terminal.ansi.bright_cyan": "#70946f",
+ "terminal.ansi.bright_white": "#2d241e",
+ "accents": [
+ "#486a59",
+ "#bb7c12",
+ "#83640f",
+ "#855a70",
+ "#237296",
+ "#397625",
+ "#b17818"
+ ],
+ "players": [
+ {
+ "cursor": "#486a59",
+ "background": "#486a59",
+ "selection": "#486a593d"
+ },
+ {
+ "cursor": "#bb7c12",
+ "background": "#bb7c12",
+ "selection": "#bb7c123d"
+ },
+ {
+ "cursor": "#83640f",
+ "background": "#83640f",
+ "selection": "#83640f3d"
+ },
+ {
+ "cursor": "#855a70",
+ "background": "#855a70",
+ "selection": "#855a703d"
+ },
+ {
+ "cursor": "#237296",
+ "background": "#237296",
+ "selection": "#2372963d"
+ },
+ {
+ "cursor": "#397625",
+ "background": "#397625",
+ "selection": "#3976253d"
+ },
+ {
+ "cursor": "#b17818",
+ "background": "#b17818",
+ "selection": "#b178183d"
+ }
+ ],
+ "syntax": {
+ "comment": {
+ "color": "#7f766a",
+ "font_style": "italic"
+ },
+ "comment.doc": {
+ "color": "#7f766a",
+ "font_style": "italic"
+ },
+ "keyword": {
+ "color": "#83640f",
+ "font_weight": 700
+ },
+ "preproc": {
+ "color": "#83640f",
+ "font_weight": 700
+ },
+ "operator": {
+ "color": "#5f5c57"
+ },
+ "punctuation": {
+ "color": "#935e79"
+ },
+ "punctuation.bracket": {
+ "color": "#935e79"
+ },
+ "punctuation.delimiter": {
+ "color": "#935e79"
+ },
+ "punctuation.list_marker": {
+ "color": "#935e79"
+ },
+ "punctuation.special": {
+ "color": "#935e79"
+ },
+ "function": {
+ "color": "#3e752b"
+ },
+ "constructor": {
+ "color": "#3e752b"
+ },
+ "property": {
+ "color": "#46703f"
+ },
+ "attribute": {
+ "color": "#46703f"
+ },
+ "label": {
+ "color": "#46703f"
+ },
+ "string": {
+ "color": "#7e5a2a"
+ },
+ "string.escape": {
+ "color": "#7e5a2a"
+ },
+ "string.regex": {
+ "color": "#7e5a2a"
+ },
+ "string.special": {
+ "color": "#7e5a2a"
+ },
+ "string.special.symbol": {
+ "color": "#7e5a2a"
+ },
+ "number": {
+ "color": "#1768a6"
+ },
+ "constant": {
+ "color": "#1768a6"
+ },
+ "constant.builtin": {
+ "color": "#1768a6"
+ },
+ "boolean": {
+ "color": "#1768a6"
+ },
+ "enum": {
+ "color": "#1768a6"
+ },
+ "variant": {
+ "color": "#1768a6"
+ },
+ "type": {
+ "color": "#237296",
+ "font_style": "italic"
+ },
+ "type.builtin": {
+ "color": "#237296",
+ "font_style": "italic"
+ },
+ "variable": {
+ "color": "#46423d"
+ },
+ "variable.special": {
+ "color": "#46423d"
+ },
+ "variable.parameter": {
+ "color": "#554a43"
+ },
+ "tag": {
+ "color": "#5f6b24"
+ },
+ "tag.doctype": {
+ "color": "#5f6b24"
+ },
+ "primary": {
+ "color": "#342d28"
+ },
+ "embedded": {
+ "color": "#342d28"
+ },
+ "hint": {
+ "color": "#766f65"
+ },
+ "predictive": {
+ "color": "#766f65"
+ },
+ "link_text": {
+ "color": "#237296"
+ },
+ "link_uri": {
+ "color": "#237296",
+ "font_style": "italic"
+ },
+ "emphasis": {
+ "color": "#342d28",
+ "font_style": "italic"
+ },
+ "emphasis.strong": {
+ "color": "#342d28",
+ "font_weight": 700
+ },
+ "title": {
+ "color": "#83640f",
+ "font_weight": 700
+ },
+ "text.literal": {
+ "color": "#7e5a2a"
+ }
+ },
+ "hint": "#766f65",
+ "hint.background": "#766f651a",
+ "hint.border": "#766f6566",
+ "hidden": "#766f65",
+ "hidden.background": "#766f651a",
+ "hidden.border": "#766f6566",
+ "ignored": "#766f65",
+ "ignored.background": "#766f651a",
+ "ignored.border": "#766f6566",
+ "predictive": "#766f65",
+ "predictive.background": "#766f651a",
+ "predictive.border": "#766f6566",
+ "unreachable": "#766f65",
+ "unreachable.background": "#766f651a",
+ "unreachable.border": "#766f6566",
+ "info": "#237296",
+ "info.background": "#2372961a",
+ "info.border": "#23729666",
+ "renamed": "#237296",
+ "renamed.background": "#2372961a",
+ "renamed.border": "#23729666",
+ "success": "#397625",
+ "success.background": "#3976251a",
+ "success.border": "#39762566",
+ "created": "#397625",
+ "created.background": "#3976251a",
+ "created.border": "#39762566",
+ "warning": "#b17818",
+ "warning.background": "#b178181a",
+ "warning.border": "#b1781866",
+ "modified": "#b17818",
+ "modified.background": "#b178181a",
+ "modified.border": "#b1781866",
+ "conflict": "#b17818",
+ "conflict.background": "#b178181a",
+ "conflict.border": "#b1781866",
+ "error": "#83640f",
+ "error.background": "#83640f1a",
+ "error.border": "#83640f66",
+ "deleted": "#83640f",
+ "deleted.background": "#83640f1a",
+ "deleted.border": "#83640f66"
+ }
+ }
+ ]
+}
diff --git a/zed/images/hearthcode-zed.png b/zed/images/hearthcode-zed.png
new file mode 100644
index 0000000..0e349c1
Binary files /dev/null and b/zed/images/hearthcode-zed.png differ
diff --git a/zed/mirror-README.md b/zed/mirror-README.md
new file mode 100644
index 0000000..f4f08b1
--- /dev/null
+++ b/zed/mirror-README.md
@@ -0,0 +1,23 @@
+# HearthCode for Zed
+
+Ember warmth or Moss structure, with Dark and Light paired for Zed. Different
+material, same reading rhythm — generated from the same calibrated color system.
+
+
+
+This repository is a generated distribution mirror. The color system,
+generators, audits, and contribution workflow live in
+[hearth-code/HearthTheme](https://github.com/hearth-code/HearthTheme). Do not
+hand-edit generated theme JSON here.
+
+## Included themes
+
+- HearthCode Moss Dark
+- HearthCode Moss Light
+- HearthCode Ember Dark
+- HearthCode Ember Light
+
+## Local development
+
+In Zed, run `zed: install dev extension` and select the directory containing
+`extension.toml`.