diff --git a/.github/workflows/release-themes.yml b/.github/workflows/release-themes.yml index e01f75f..35f655a 100644 --- a/.github/workflows/release-themes.yml +++ b/.github/workflows/release-themes.yml @@ -116,6 +116,13 @@ jobs: - name: Package Home Assistant run: cp themes/warm-burnout.yaml warm-burnout-home-assistant.yaml + - name: Package Obsidian + run: | + cd obsidian + zip -j ../warm-burnout-obsidian.zip \ + theme.css \ + manifest.json + - name: Package JetBrains run: | cd jetbrains @@ -137,6 +144,7 @@ jobs: warm-burnout-tmux.zip warm-burnout-eza.zip warm-burnout-home-assistant.yaml + warm-burnout-obsidian.zip jetbrains/warm-burnout-theme.jar - name: Publish to JetBrains Marketplace @@ -149,3 +157,36 @@ jobs: https://plugins.jetbrains.com/api/updates/upload env: JETBRAINS_TOKEN: ${{ secrets.JETBRAINS_TOKEN }} + + sync-obsidian-mirror: + needs: [validate] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Sync to mirror repo + env: + MIRROR_DEPLOY_KEY: ${{ secrets.OBSIDIAN_MIRROR_DEPLOY_KEY }} + run: | + mkdir -p ~/.ssh + echo "$MIRROR_DEPLOY_KEY" > ~/.ssh/deploy_key + chmod 600 ~/.ssh/deploy_key + export GIT_SSH_COMMAND="ssh -i ~/.ssh/deploy_key -o StrictHostKeyChecking=no" + + git clone git@github.com:felipefdl/warm-burnout-obsidian.git mirror + cp obsidian/theme.css mirror/ + cp obsidian/manifest.json mirror/ + cp obsidian/README.md mirror/ + cp LICENSE mirror/ + + cd mirror + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add -A + if git diff --cached --quiet; then + echo "No changes to sync" + else + git commit -m "Sync from warm-burnout ${GITHUB_REF_NAME}" + git tag "${GITHUB_REF_NAME}" + git push origin main --tags + fi diff --git a/AGENTS.md b/AGENTS.md index fc12677..37d27c1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -39,6 +39,7 @@ warm-burnout/ windows_terminal.rs # Windows Terminal theme validation tests tmux.rs # tmux theme validation tests zsh.rs # Zsh theme validation tests + obsidian.rs # Obsidian theme validation tests .github/workflows/ validate.yml # CI: run theme validation on push/PR release-vscode.yml # VS Code extension release workflow @@ -133,6 +134,11 @@ warm-burnout/ light.yml # Light variant README.md # eza install instructions AGENTS.md # eza-specific agent rules + obsidian/ # Obsidian community theme + theme.css # Dark + light variants (CSS custom properties) + manifest.json # Community theme manifest + README.md # Obsidian install instructions + AGENTS.md # Obsidian-specific agent rules screenshots/ # Theme preview screenshots AGENTS.md # Screenshot-specific agent rules generate.mjs # Playwright script to render HTML -> PNG diff --git a/README.md b/README.md index e4a07c8..be9b90c 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,7 @@ Inspired by materials that age well. Unlike your eyes. | Zsh | Available | [`zsh/`](zsh/) | | Home Assistant | Available | [`home-assistant/`](home-assistant/) | | eza | Available | [`eza/`](eza/) | +| Obsidian | Available | [`obsidian/`](obsidian/) | Each platform lives in its own directory with its own README, build process, and release workflow. diff --git a/obsidian/AGENTS.md b/obsidian/AGENTS.md new file mode 100644 index 0000000..feace28 --- /dev/null +++ b/obsidian/AGENTS.md @@ -0,0 +1,54 @@ +# Obsidian -- Agent Instructions + +## Platform Reference + +See the root [`AGENTS.md`](../AGENTS.md) for the canonical palette, design principles, and brand rules. Do not duplicate palette tables here. + +## Obsidian Theme Format + +- Obsidian community themes consist of `theme.css` and `manifest.json` at the repo/directory root. +- Both dark and light variants live in a single `theme.css`, switched via `.theme-dark` / `.theme-light` body class selectors. +- `manifest.json` contains name, version, minAppVersion, author, and optional authorUrl. + +## CSS Architecture + +The theme uses a four-layer CSS custom property system: + +1. **Palette layer** (`--wb-*`): Canonical hex values inside `.theme-dark` / `.theme-light`. Only place raw hex appears. Test harness reads from these. +2. **Base mapping**: Maps `--wb-*` into Obsidian's `--color-base-*` ramp (13 steps) and `--color-*` extended colors. +3. **Code syntax**: `--code-*` variables mapped to `--wb-*` for both CodeMirror 6 and Prism.js. Additional `.token.*` rules for reading view. +4. **Warmth tweaks**: Warm shadows, scrollbar tints, softer radii. No layout changes. + +## Color Variable Extraction + +The test harness uses `obsidian_color(src, variant, key)` to extract `--wb-{key}: #hex;` from inside the `.theme-{variant}` block. When adding new palette colors, add them as `--wb-*` declarations in both variant blocks. + +## Surface Ramp + +The `--color-base-*` scale uses 13 steps (00, 05, 10, 20, 25, 30, 35, 40, 50, 60, 70, 100). All intermediates carry a warm undertone. In dark mode, 00 = deepest surface, 100 = primary text. In light mode, 00 = lightest surface, 100 = primary text. + +## Heading Colors + +H1 through H6 are mapped to palette materials by visual weight: amber, burnt orange, aged brass, terra cotta, steel patina, warm stone. Set via `--h1-color` through `--h6-color` in each variant block. + +## Distribution + +The theme is distributed via a mirror repo (`felipefdl/warm-burnout-obsidian`) that CI syncs on tag push. The Obsidian community directory pulls from that mirror. Source of truth is always this monorepo. + +## File Structure + +``` +obsidian/ + theme.css # Single CSS file, both dark + light variants + manifest.json # Obsidian theme manifest + README.md # Install instructions + AGENTS.md # This file +``` + +## Rules + +1. Every hex value in `--wb-*` declarations must come from the canonical palette. No approximations. +2. Surface ramp intermediates must carry warm undertone (R > G > B in hex channels). +3. Do not add blues outside of the steel patina type accent. +4. Keep both `.theme-dark` and `.theme-light` blocks in sync: same `--wb-*` variable set, different values. +5. Test changes with `cargo test --test obsidian`. diff --git a/obsidian/README.md b/obsidian/README.md new file mode 100644 index 0000000..f994c40 --- /dev/null +++ b/obsidian/README.md @@ -0,0 +1,38 @@ +# Warm Burnout for Obsidian + +Your second brain was running on factory-default colors. Cold blues, harsh whites, zero consideration for 2am rabbit holes through your note graph. Fixed. + +Full community theme for Obsidian with dark and light variants. Warm palette, contrast-audited, zero blues in the chrome. Headings follow a natural energy gradient from amber down to warm stone. + +## Install + +### Community Themes (recommended) + +1. Open Settings > Appearance > Themes +2. Click "Manage" and search for **Warm Burnout** +3. Install and activate + +### Manual + +1. Download `theme.css` and `manifest.json` from the [latest release](https://github.com/felipefdl/warm-burnout/releases) +2. Create a folder called `Warm Burnout` inside your vault's `.obsidian/themes/` directory +3. Place both files in that folder +4. Open Settings > Appearance > Themes and select **Warm Burnout** + +## What This Themes + +- **Surface hierarchy**: 13-step warm ramp from deep brown-black to warm cream. No neutral grays anywhere. +- **Headings**: H1 through H6 mapped to palette materials (amber, burnt orange, aged brass, terra cotta, steel patina, warm stone). Visual weight decreases naturally. +- **Code blocks**: Full syntax palette in both editing and reading views. Functions = amber, keywords = burnt orange, strings = dried sage, types = steel patina. +- **Accent**: Copper rust on all interactive elements, links, and active states. +- **Warm tweaks**: Tinted shadows, warm scrollbar tracks, soft selection highlights. No layout changes. + +## Palette + +Both variants derive from the canonical Warm Burnout palette defined in the root [`AGENTS.md`](../AGENTS.md). + +Dark background: `#1a1510`. Light background: `#F5EDE0`. Accent: `#b8522e` (copper rust). + +## Requirements + +Obsidian 1.0.0 or later. diff --git a/obsidian/manifest.json b/obsidian/manifest.json new file mode 100644 index 0000000..7ddf984 --- /dev/null +++ b/obsidian/manifest.json @@ -0,0 +1,7 @@ +{ + "name": "Warm Burnout", + "version": "1.4.2", + "minAppVersion": "1.0.0", + "author": "Felipe Lima", + "authorUrl": "https://warmburnout.com" +} diff --git a/obsidian/theme.css b/obsidian/theme.css new file mode 100644 index 0000000..e3313fb --- /dev/null +++ b/obsidian/theme.css @@ -0,0 +1,432 @@ +/* ========================================================================== + Warm Burnout for Obsidian + Warm, contrast-audited color theme. Dark + Light. + https://warmburnout.com + ========================================================================== */ + +/* -- Dark variant --------------------------------------------------------- */ +.theme-dark { + /* Palette (canonical hex values, test harness reads from these) */ + --wb-bg: #1a1510; + --wb-fg: #bfbdb6; + --wb-accent: #b8522e; + --wb-cursor: #f5c56e; + --wb-amber: #ffb454; + --wb-burnt-orange: #ff8f40; + --wb-operator: #f29668; + --wb-terra-cotta: #dc9e92; + --wb-dried-sage: #b4bc78; + --wb-verdigris: #96b898; + --wb-dusty-mauve: #d4a8b8; + --wb-coral: #ec9878; + --wb-warm-stone: #b4a89c; + --wb-aged-brass: #deb074; + --wb-steel-patina: #90aec0; + --wb-gold: #e6c08a; + --wb-error: #f49090; + + /* Surface ramp (warm undertone throughout, no neutral grays) */ + --color-base-00: #14120f; + --color-base-05: #1a1510; + --color-base-10: #1f1d17; + --color-base-20: #2a2520; + --color-base-25: #302a22; + --color-base-30: #3a342c; + --color-base-35: #443d34; + --color-base-40: #5a5348; + --color-base-50: #6e665c; + --color-base-60: #8a8278; + --color-base-70: #9e968c; + --color-base-100: #bfbdb6; + + /* Extended colors */ + --color-red: var(--wb-error); + --color-red-rgb: 244, 144, 144; + --color-orange: var(--wb-burnt-orange); + --color-orange-rgb: 255, 143, 64; + --color-yellow: var(--wb-amber); + --color-yellow-rgb: 255, 180, 84; + --color-green: var(--wb-dried-sage); + --color-green-rgb: 180, 188, 120; + --color-cyan: var(--wb-verdigris); + --color-cyan-rgb: 150, 184, 152; + --color-blue: var(--wb-steel-patina); + --color-blue-rgb: 144, 174, 192; + --color-purple: var(--wb-dusty-mauve); + --color-purple-rgb: 212, 168, 184; + --color-pink: var(--wb-coral); + --color-pink-rgb: 236, 152, 120; + + /* Headings */ + --h1-color: var(--wb-amber); + --h2-color: var(--wb-burnt-orange); + --h3-color: var(--wb-aged-brass); + --h4-color: var(--wb-terra-cotta); + --h5-color: var(--wb-steel-patina); + --h6-color: var(--wb-warm-stone); + + /* Selection and highlights */ + --text-selection: rgba(245, 197, 110, 0.2); + --text-highlight-bg: rgba(245, 197, 110, 0.15); + + /* Ribbon (left icon strip) */ + --ribbon-background: #14120f; + --ribbon-background-collapsed: #14120f; + + /* Sidebar */ + --background-secondary: #16130f; + --background-secondary-alt: #1a1510; + + /* Navigation items */ + --nav-item-color: var(--color-base-60); + --nav-item-color-hover: var(--wb-fg); + --nav-item-color-active: var(--wb-fg); + --nav-item-color-selected: var(--wb-fg); + --nav-item-background-hover: rgba(184, 82, 46, 0.08); + --nav-item-background-active: rgba(184, 82, 46, 0.12); + --nav-item-background-selected: rgba(184, 82, 46, 0.15); + + /* Tabs */ + --tab-background-active: #1a1510; + --tab-text-color: var(--color-base-50); + --tab-text-color-focused: var(--wb-fg); + --tab-text-color-focused-active: var(--wb-fg); + --tab-text-color-focused-active-current: var(--wb-fg); + --tab-outline-color: transparent; + --tab-stacked-header-height: 36px; + --tab-container-background: #14120f; + --tab-divider-color: transparent; + --tab-curve: 6px; + + /* Titlebar */ + --titlebar-background: #14120f; + --titlebar-background-focused: #14120f; + --titlebar-text-color: var(--color-base-60); + + /* Status bar */ + --status-bar-background: #14120f; + --status-bar-text-color: var(--color-base-50); + --status-bar-border-color: transparent; + + /* Dividers and borders */ + --divider-color: rgba(191, 189, 182, 0.06); + --tab-outline-color: transparent; + --prompt-border-color: var(--color-base-25); + --background-modifier-border: var(--color-base-20); + --background-modifier-border-hover: var(--color-base-30); + --background-modifier-border-focus: var(--wb-accent); + + /* Interactive states */ + --interactive-normal: var(--color-base-10); + --interactive-hover: var(--color-base-20); + --interactive-accent: var(--wb-accent); + --interactive-accent-hover: #c4603a; + --background-modifier-hover: rgba(184, 82, 46, 0.08); + + /* Icons */ + --icon-color: var(--color-base-50); + --icon-color-hover: var(--wb-fg); + --icon-color-active: var(--wb-accent); + --icon-color-focused: var(--color-base-60); + --icon-opacity: 0.85; + --icon-opacity-hover: 1; + --icon-opacity-active: 1; + + /* Checkbox accent */ + --checkbox-color: var(--wb-accent); + --checkbox-color-hover: #c4603a; + --checkbox-border-color: var(--color-base-35); + + /* Tag pills */ + --tag-color: var(--wb-accent); + --tag-color-hover: #c4603a; + --tag-background: rgba(184, 82, 46, 0.1); + --tag-background-hover: rgba(184, 82, 46, 0.18); +} + +/* -- Light variant -------------------------------------------------------- */ +.theme-light { + /* Palette (canonical hex values, test harness reads from these) */ + --wb-bg: #f5ede0; + --wb-fg: #3a3630; + --wb-accent: #b8522e; + --wb-cursor: #8a6600; + --wb-amber: #855700; + --wb-burnt-orange: #924800; + --wb-operator: #8f4418; + --wb-terra-cotta: #8e4632; + --wb-dried-sage: #4d5c1a; + --wb-verdigris: #286a48; + --wb-dusty-mauve: #7e4060; + --wb-coral: #883850; + --wb-warm-stone: #544c40; + --wb-aged-brass: #74501c; + --wb-steel-patina: #285464; + --wb-gold: #7a5a1c; + --wb-error: #b03434; + + /* Surface ramp (warm undertone throughout, no neutral grays) */ + --color-base-00: #f5ede0; + --color-base-05: #ede5d8; + --color-base-10: #e5ddd0; + --color-base-20: #d5cbbd; + --color-base-25: #c8bfb0; + --color-base-30: #b8ae9e; + --color-base-35: #a89e8e; + --color-base-40: #968c7c; + --color-base-50: #7e7568; + --color-base-60: #665e52; + --color-base-70: #524a40; + --color-base-100: #3a3630; + + /* Extended colors */ + --color-red: var(--wb-error); + --color-red-rgb: 176, 52, 52; + --color-orange: var(--wb-burnt-orange); + --color-orange-rgb: 146, 72, 0; + --color-yellow: var(--wb-amber); + --color-yellow-rgb: 133, 87, 0; + --color-green: var(--wb-dried-sage); + --color-green-rgb: 77, 92, 26; + --color-cyan: var(--wb-verdigris); + --color-cyan-rgb: 40, 106, 72; + --color-blue: var(--wb-steel-patina); + --color-blue-rgb: 40, 84, 100; + --color-purple: var(--wb-dusty-mauve); + --color-purple-rgb: 126, 64, 96; + --color-pink: var(--wb-coral); + --color-pink-rgb: 136, 56, 80; + + /* Headings */ + --h1-color: var(--wb-amber); + --h2-color: var(--wb-burnt-orange); + --h3-color: var(--wb-aged-brass); + --h4-color: var(--wb-terra-cotta); + --h5-color: var(--wb-steel-patina); + --h6-color: var(--wb-warm-stone); + + /* Selection and highlights */ + --text-selection: rgba(138, 102, 0, 0.15); + --text-highlight-bg: rgba(138, 102, 0, 0.12); + + /* Ribbon (left icon strip) */ + --ribbon-background: #ebe3d6; + --ribbon-background-collapsed: #ebe3d6; + + /* Sidebar */ + --background-secondary: #efe7da; + --background-secondary-alt: #f5ede0; + + /* Navigation items */ + --nav-item-color: var(--color-base-60); + --nav-item-color-hover: var(--wb-fg); + --nav-item-color-active: var(--wb-fg); + --nav-item-color-selected: var(--wb-fg); + --nav-item-background-hover: rgba(184, 82, 46, 0.06); + --nav-item-background-active: rgba(184, 82, 46, 0.1); + --nav-item-background-selected: rgba(184, 82, 46, 0.12); + + /* Tabs */ + --tab-background-active: #f5ede0; + --tab-text-color: var(--color-base-50); + --tab-text-color-focused: var(--wb-fg); + --tab-text-color-focused-active: var(--wb-fg); + --tab-text-color-focused-active-current: var(--wb-fg); + --tab-outline-color: transparent; + --tab-stacked-header-height: 36px; + --tab-container-background: #ebe3d6; + --tab-divider-color: transparent; + --tab-curve: 6px; + + /* Titlebar */ + --titlebar-background: #ebe3d6; + --titlebar-background-focused: #ebe3d6; + --titlebar-text-color: var(--color-base-60); + + /* Status bar */ + --status-bar-background: #ebe3d6; + --status-bar-text-color: var(--color-base-50); + --status-bar-border-color: transparent; + + /* Dividers and borders */ + --divider-color: rgba(58, 54, 48, 0.08); + --tab-outline-color: transparent; + --prompt-border-color: var(--color-base-25); + --background-modifier-border: var(--color-base-20); + --background-modifier-border-hover: var(--color-base-30); + --background-modifier-border-focus: var(--wb-accent); + + /* Interactive states */ + --interactive-normal: var(--color-base-10); + --interactive-hover: var(--color-base-20); + --interactive-accent: var(--wb-accent); + --interactive-accent-hover: #a04828; + --background-modifier-hover: rgba(184, 82, 46, 0.06); + + /* Icons */ + --icon-color: var(--color-base-50); + --icon-color-hover: var(--wb-fg); + --icon-color-active: var(--wb-accent); + --icon-color-focused: var(--color-base-60); + --icon-opacity: 0.85; + --icon-opacity-hover: 1; + --icon-opacity-active: 1; + + /* Checkbox accent */ + --checkbox-color: var(--wb-accent); + --checkbox-color-hover: #a04828; + --checkbox-border-color: var(--color-base-35); + + /* Tag pills */ + --tag-color: var(--wb-accent); + --tag-color-hover: #a04828; + --tag-background: rgba(184, 82, 46, 0.08); + --tag-background-hover: rgba(184, 82, 46, 0.14); +} + +/* -- Shared (resolves per-variant via cascade) ---------------------------- */ +body { + /* Accent: copper rust HSL decomposition of #b8522e */ + --accent-h: 16; + --accent-s: 60%; + --accent-l: 45%; + + /* Code syntax highlighting (CodeMirror 6 + Prism.js shared tokens) */ + --code-background: var(--color-base-00); + --code-normal: var(--wb-fg); + --code-function: var(--wb-amber); + --code-keyword: var(--wb-burnt-orange); + --code-string: var(--wb-dried-sage); + --code-comment: var(--wb-warm-stone); + --code-tag: var(--wb-terra-cotta); + --code-value: var(--wb-dusty-mauve); + --code-property: var(--wb-aged-brass); + --code-important: var(--wb-verdigris); + --code-operator: var(--wb-operator); + --code-punctuation: var(--color-base-60); + + /* Warm shadows */ + --background-modifier-box-shadow: rgba(26, 21, 16, 0.3); + --input-shadow: 0 1px 2px rgba(26, 21, 16, 0.15); + --shadow-s: 0px 1px 2px rgba(26, 21, 16, 0.12), 0px 3px 6px rgba(26, 21, 16, 0.08); + --shadow-l: 0px 2px 4px rgba(26, 21, 16, 0.12), 0px 8px 24px rgba(26, 21, 16, 0.16); + + /* Warm scrollbars */ + --scrollbar-bg: var(--color-base-10); + --scrollbar-thumb-bg: var(--color-base-35); + --scrollbar-active-thumb-bg: var(--color-base-40); + + /* Softer radii */ + --radius-s: 4px; + --radius-m: 6px; + --radius-l: 10px; +} + +/* -- UI chrome refinements ------------------------------------------------ */ + +/* Ribbon icons: warm tint on hover, accent on active */ +.workspace-ribbon .sidebar-toggle-button, +.workspace-ribbon .clickable-icon { + border-radius: var(--radius-m); + transition: background 0.15s, color 0.15s; +} + +/* Tab bar: active tab stands out with bottom accent border */ +.workspace-tab-header.is-active { + border-bottom: 2px solid var(--wb-accent); +} + +.workspace-tab-header:not(.is-active) { + opacity: 0.7; +} + +.workspace-tab-header:not(.is-active):hover { + opacity: 1; +} + +/* Nav items: smoother hover, rounded selection */ +.nav-file-title, +.nav-folder-title { + border-radius: var(--radius-s); + transition: background 0.12s, color 0.12s; +} + +/* Status bar: subtle top border */ +.status-bar { + border-top: 1px solid var(--divider-color); +} + +/* Sidebar section headers */ +.nav-header { + text-transform: uppercase; + font-size: 0.65em; + letter-spacing: 0.08em; + color: var(--color-base-50); +} + +/* Vault name in sidebar */ +.nav-buttons-container + .tree-item .nav-folder-title-content { + font-weight: 600; +} + +/* -- Prism.js token overrides (reading view) ------------------------------ */ +.markdown-reading-view .token.keyword { + color: var(--wb-burnt-orange); + font-weight: bold; +} + +.markdown-reading-view .token.function { + color: var(--wb-amber); +} + +.markdown-reading-view .token.string, +.markdown-reading-view .token.char, +.markdown-reading-view .token.attr-value { + color: var(--wb-dried-sage); +} + +.markdown-reading-view .token.number, +.markdown-reading-view .token.boolean, +.markdown-reading-view .token.constant { + color: var(--wb-dusty-mauve); +} + +.markdown-reading-view .token.comment { + color: var(--wb-warm-stone); + font-style: italic; +} + +.markdown-reading-view .token.operator { + color: var(--wb-operator); +} + +.markdown-reading-view .token.punctuation { + color: var(--color-base-60); +} + +.markdown-reading-view .token.property { + color: var(--wb-aged-brass); + font-style: italic; +} + +.markdown-reading-view .token.class-name { + color: var(--wb-steel-patina); + font-style: italic; +} + +.markdown-reading-view .token.tag { + color: var(--wb-terra-cotta); + font-weight: bold; +} + +.markdown-reading-view .token.regex { + color: var(--wb-verdigris); +} + +.markdown-reading-view .token.symbol { + color: var(--wb-terra-cotta); +} + +.markdown-reading-view .token.selector { + color: var(--wb-coral); +} diff --git a/site/index.html b/site/index.html index 1e0f000..1c2b2e4 100644 --- a/site/index.html +++ b/site/index.html @@ -5,14 +5,14 @@ Warm Burnout: warm color theme for VS Code, JetBrains, Neovim, Ghostty, and more - + - + @@ -25,7 +25,7 @@ - + @@ -35,7 +35,7 @@ "@context": "https://schema.org", "@type": "SoftwareApplication", "name": "Warm Burnout", - "description": "A warm, contrast-audited color theme suite for developers. Fully warm palette, minimal blue light, WCAG AAA dark and AA light variants across 15 platforms.", + "description": "A warm, contrast-audited color theme suite for developers. Fully warm palette, minimal blue light, WCAG AAA dark and AA light variants across 16 platforms.", "url": "https://warmburnout.com/", "applicationCategory": "DeveloperApplication", "operatingSystem": "Windows, macOS, Linux", @@ -51,7 +51,7 @@ }, "license": "https://opensource.org/licenses/MIT", "screenshot": "https://warmburnout.com/og-image.png", - "softwareRequirements": "VS Code, Open VSX (Antigravity, Cursor, Windsurf, Kiro, VSCodium), JetBrains IDEs, Neovim, Ghostty, Zed, Xcode, iTerm2, Windows Terminal, tmux, Starship, Zsh, Home Assistant, or eza", + "softwareRequirements": "VS Code, Open VSX (Antigravity, Cursor, Windsurf, Kiro, VSCodium), JetBrains IDEs, Neovim, Ghostty, Zed, Xcode, iTerm2, Windows Terminal, tmux, Starship, Zsh, Home Assistant, eza, or Obsidian", "image": "https://warmburnout.com/og-image.png" } @@ -374,7 +374,7 @@

The palette

Consistent damage across all platforms

-

15 platforms. The burnout is spreading.

+

16 platforms. The burnout is spreading.

@@ -462,6 +462,12 @@

Consistent Available + +

Obsidian

+

Notes

+ Available +
+

diff --git a/tests/brand.rs b/tests/brand.rs index 30985e1..cac9847 100644 --- a/tests/brand.rs +++ b/tests/brand.rs @@ -10,6 +10,7 @@ const READMES: &[(&str, &str)] = &[ ("tmux", include_str!("../tmux/README.md")), ("iterm2", include_str!("../iterm2/README.md")), ("jetbrains", include_str!("../jetbrains/README.md")), + ("obsidian", include_str!("../obsidian/README.md")), ]; #[test] @@ -48,11 +49,16 @@ fn no_theme_file_uses_patina_as_label() { "jetbrains/light-theme", include_str!("../jetbrains/Warm Burnout Islands Light.theme.json"), ), + ("obsidian/theme", include_str!("../obsidian/theme.css")), ]; for (name, content) in theme_files { for line in content.lines() { let lower = line.to_lowercase(); - if lower.contains("patina") && !lower.contains("steel_patina") && !lower.contains("steel patina") { + if lower.contains("patina") + && !lower.contains("steel_patina") + && !lower.contains("steel patina") + && !lower.contains("steel-patina") + { panic!("{name}: line uses 'Patina' as brand name (should be 'Warm Burnout'): {line}"); } } diff --git a/tests/canonical.rs b/tests/canonical.rs index 380483b..fb84cd0 100644 --- a/tests/canonical.rs +++ b/tests/canonical.rs @@ -2,8 +2,8 @@ mod common; use common::{ ghostty_ansi_color, ghostty_color, hex_to_lower, home_assistant_color, iterm2_color, jetbrains_attribute, - jetbrains_color, nvim_palette_color, starship_palette_color, tmux_option_value, tmux_style_fg, vscode_color, - windows_terminal_color, xcode_color, xcode_syntax_color, zed_editor_color, + jetbrains_color, nvim_palette_color, obsidian_color, starship_palette_color, tmux_option_value, tmux_style_fg, + vscode_color, windows_terminal_color, xcode_color, xcode_syntax_color, zed_editor_color, }; fn zsh_foreground(src: &str) -> Option { @@ -918,3 +918,119 @@ fn light_zsh_error_matches_starship() { ); assert_eq!(zsh, starship, "light error: zsh={zsh} starship={starship}"); } + +// -- Obsidian cross-platform consistency -- + +const OBSIDIAN_THEME: &str = include_str!("../obsidian/theme.css"); + +#[test] +fn dark_background_obsidian_matches_vscode() { + let obsidian = obsidian_color(OBSIDIAN_THEME, "dark", "bg"); + let vscode = vscode_color( + include_str!("../vscode/themes/warm-burnout-dark.json"), + "editor.background", + ); + assert_eq!(obsidian, vscode, "dark background: obsidian={obsidian} vscode={vscode}"); +} + +#[test] +fn light_background_obsidian_matches_vscode() { + let obsidian = obsidian_color(OBSIDIAN_THEME, "light", "bg"); + let vscode = vscode_color( + include_str!("../vscode/themes/warm-burnout-light.json"), + "editor.background", + ); + assert_eq!( + obsidian, vscode, + "light background: obsidian={obsidian} vscode={vscode}" + ); +} + +#[test] +fn dark_foreground_obsidian_matches_vscode() { + let obsidian = obsidian_color(OBSIDIAN_THEME, "dark", "fg"); + let vscode = vscode_color( + include_str!("../vscode/themes/warm-burnout-dark.json"), + "editor.foreground", + ); + assert_eq!(obsidian, vscode, "dark foreground: obsidian={obsidian} vscode={vscode}"); +} + +#[test] +fn light_foreground_obsidian_matches_vscode() { + let obsidian = obsidian_color(OBSIDIAN_THEME, "light", "fg"); + let vscode = vscode_color( + include_str!("../vscode/themes/warm-burnout-light.json"), + "editor.foreground", + ); + assert_eq!( + obsidian, vscode, + "light foreground: obsidian={obsidian} vscode={vscode}" + ); +} + +#[test] +fn dark_accent_obsidian_matches_canonical() { + let obsidian = obsidian_color(OBSIDIAN_THEME, "dark", "accent"); + assert_eq!(obsidian, "#b8522e", "dark accent should be canonical copper rust"); +} + +#[test] +fn light_accent_obsidian_matches_canonical() { + let obsidian = obsidian_color(OBSIDIAN_THEME, "light", "accent"); + assert_eq!(obsidian, "#b8522e", "light accent should be canonical copper rust"); +} + +#[test] +fn dark_background_obsidian_matches_ghostty() { + let obsidian = obsidian_color(OBSIDIAN_THEME, "dark", "bg"); + let ghostty = ghostty_color(include_str!("../ghostty/warm-burnout-dark"), "background"); + assert_eq!( + obsidian, ghostty, + "dark background: obsidian={obsidian} ghostty={ghostty}" + ); +} + +#[test] +fn light_background_obsidian_matches_ghostty() { + let obsidian = obsidian_color(OBSIDIAN_THEME, "light", "bg"); + let ghostty = ghostty_color(include_str!("../ghostty/warm-burnout-light"), "background"); + assert_eq!( + obsidian, ghostty, + "light background: obsidian={obsidian} ghostty={ghostty}" + ); +} + +#[test] +fn dark_foreground_obsidian_matches_ghostty() { + let obsidian = obsidian_color(OBSIDIAN_THEME, "dark", "fg"); + let ghostty = ghostty_color(include_str!("../ghostty/warm-burnout-dark"), "foreground"); + assert_eq!( + obsidian, ghostty, + "dark foreground: obsidian={obsidian} ghostty={ghostty}" + ); +} + +#[test] +fn light_foreground_obsidian_matches_ghostty() { + let obsidian = obsidian_color(OBSIDIAN_THEME, "light", "fg"); + let ghostty = ghostty_color(include_str!("../ghostty/warm-burnout-light"), "foreground"); + assert_eq!( + obsidian, ghostty, + "light foreground: obsidian={obsidian} ghostty={ghostty}" + ); +} + +#[test] +fn dark_cursor_obsidian_matches_ghostty() { + let obsidian = obsidian_color(OBSIDIAN_THEME, "dark", "cursor"); + let ghostty = ghostty_color(include_str!("../ghostty/warm-burnout-dark"), "cursor-color"); + assert_eq!(obsidian, ghostty, "dark cursor: obsidian={obsidian} ghostty={ghostty}"); +} + +#[test] +fn light_cursor_obsidian_matches_ghostty() { + let obsidian = obsidian_color(OBSIDIAN_THEME, "light", "cursor"); + let ghostty = ghostty_color(include_str!("../ghostty/warm-burnout-light"), "cursor-color"); + assert_eq!(obsidian, ghostty, "light cursor: obsidian={obsidian} ghostty={ghostty}"); +} diff --git a/tests/common.rs b/tests/common.rs index 15c6364..b702f08 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -327,3 +327,47 @@ pub fn nvim_palette_keys(src: &str) -> Vec { }) .collect() } + +/// Extract a color from an Obsidian theme CSS file. +/// Finds `--wb-{key}: #hex;` inside the `.theme-{variant}` block. +pub fn obsidian_color(src: &str, variant: &str, key: &str) -> String { + let selector = format!(".theme-{variant}"); + let var_decl = format!("--wb-{}:", key); + + let sel_pos = src + .find(&selector) + .unwrap_or_else(|| panic!("no {selector} block in obsidian theme")); + let rest = &src[sel_pos..]; + let brace_pos = rest + .find('{') + .unwrap_or_else(|| panic!("no opening brace after {selector}")); + let block_start = &rest[brace_pos + 1..]; + + let mut depth = 1; + let mut block_end = 0; + for (i, c) in block_start.char_indices() { + match c { + '{' => depth += 1, + '}' => { + depth -= 1; + if depth == 0 { + block_end = i; + break; + } + } + _ => {} + } + } + let block = &block_start[..block_end]; + + block + .lines() + .find(|l| l.trim().starts_with(&var_decl)) + .and_then(|l| { + l.split_once(':').map(|(_, v)| { + let v = v.trim().trim_end_matches(';').trim(); + hex_to_lower(v) + }) + }) + .unwrap_or_else(|| panic!("no --wb-{key} in {selector} block")) +} diff --git a/tests/obsidian.rs b/tests/obsidian.rs new file mode 100644 index 0000000..f7a3017 --- /dev/null +++ b/tests/obsidian.rs @@ -0,0 +1,164 @@ +mod common; + +use common::{extract_hex_colors, is_valid_hex, obsidian_color}; + +const THEME: &str = include_str!("../obsidian/theme.css"); +const MANIFEST: &str = include_str!("../obsidian/manifest.json"); + +// -- Structure -- + +#[test] +fn has_theme_dark_block() { + assert!(THEME.contains(".theme-dark"), "theme.css must have a .theme-dark block"); +} + +#[test] +fn has_theme_light_block() { + assert!( + THEME.contains(".theme-light"), + "theme.css must have a .theme-light block" + ); +} + +// -- All hex colors valid -- + +#[test] +fn all_hex_colors_are_valid() { + for (line, hex) in extract_hex_colors(THEME) { + assert!(is_valid_hex(hex), "line {line}: invalid hex: {hex}"); + } +} + +// -- Dark palette values match canonical -- + +const DARK_PALETTE: &[(&str, &str)] = &[ + ("bg", "#1a1510"), + ("fg", "#bfbdb6"), + ("accent", "#b8522e"), + ("cursor", "#f5c56e"), + ("amber", "#ffb454"), + ("burnt-orange", "#ff8f40"), + ("operator", "#f29668"), + ("terra-cotta", "#dc9e92"), + ("dried-sage", "#b4bc78"), + ("verdigris", "#96b898"), + ("dusty-mauve", "#d4a8b8"), + ("coral", "#ec9878"), + ("warm-stone", "#b4a89c"), + ("aged-brass", "#deb074"), + ("steel-patina", "#90aec0"), + ("gold", "#e6c08a"), + ("error", "#f49090"), +]; + +#[test] +fn dark_palette_values() { + for (key, expected) in DARK_PALETTE { + let actual = obsidian_color(THEME, "dark", key); + assert_eq!( + actual, *expected, + "dark --wb-{key}: expected={expected} actual={actual}" + ); + } +} + +// -- Light palette values match canonical -- + +const LIGHT_PALETTE: &[(&str, &str)] = &[ + ("bg", "#f5ede0"), + ("fg", "#3a3630"), + ("accent", "#b8522e"), + ("cursor", "#8a6600"), + ("amber", "#855700"), + ("burnt-orange", "#924800"), + ("operator", "#8f4418"), + ("terra-cotta", "#8e4632"), + ("dried-sage", "#4d5c1a"), + ("verdigris", "#286a48"), + ("dusty-mauve", "#7e4060"), + ("coral", "#883850"), + ("warm-stone", "#544c40"), + ("aged-brass", "#74501c"), + ("steel-patina", "#285464"), + ("gold", "#7a5a1c"), + ("error", "#b03434"), +]; + +#[test] +fn light_palette_values() { + for (key, expected) in LIGHT_PALETTE { + let actual = obsidian_color(THEME, "light", key); + assert_eq!( + actual, *expected, + "light --wb-{key}: expected={expected} actual={actual}" + ); + } +} + +// -- Both variants have the same palette keys -- + +#[test] +fn dark_and_light_have_same_palette_keys() { + let dark_keys: Vec<&str> = DARK_PALETTE.iter().map(|(k, _)| *k).collect(); + let light_keys: Vec<&str> = LIGHT_PALETTE.iter().map(|(k, _)| *k).collect(); + assert_eq!(dark_keys, light_keys, "dark and light palette key sets must match"); +} + +// -- Manifest -- + +#[test] +fn manifest_is_valid_json() { + let _: serde_json::Value = serde_json::from_str(MANIFEST).expect("manifest.json is not valid JSON"); +} + +#[test] +fn manifest_has_required_fields() { + let v: serde_json::Value = serde_json::from_str(MANIFEST).unwrap(); + assert!(v["name"].is_string(), "manifest missing 'name'"); + assert!(v["version"].is_string(), "manifest missing 'version'"); + assert!(v["minAppVersion"].is_string(), "manifest missing 'minAppVersion'"); + assert!(v["author"].is_string(), "manifest missing 'author'"); +} + +#[test] +fn manifest_name_is_warm_burnout() { + let v: serde_json::Value = serde_json::from_str(MANIFEST).unwrap(); + assert_eq!(v["name"].as_str().unwrap(), "Warm Burnout"); +} + +// -- Code syntax variables reference palette -- + +const CODE_VARS: &[&str] = &[ + "--code-background", + "--code-normal", + "--code-function", + "--code-keyword", + "--code-string", + "--code-comment", + "--code-tag", + "--code-value", + "--code-property", + "--code-important", + "--code-operator", + "--code-punctuation", +]; + +#[test] +fn has_all_code_syntax_variables() { + for var in CODE_VARS { + assert!( + THEME.contains(&format!("{var}:")), + "theme.css missing code syntax variable: {var}" + ); + } +} + +// -- Heading colors set for H1-H6 -- + +#[test] +fn has_heading_color_variables() { + for n in 1..=6 { + let var = format!("--h{n}-color"); + assert!(THEME.contains(&format!("{var}:")), "theme.css missing {var}"); + } +}