| 1 | # Reasonix Theme Pack V2 |
| 2 | |
| 3 | Native theme packs for the Reasonix desktop app. Packs are controlled skins: |
| 4 | semantic color tokens, density/corner recipes, and optional local images for |
| 5 | the home and task/workspace scenes. They **cannot** run CSS, JavaScript, fonts, |
| 6 | remote URLs, or SVG scripts. V1 packs remain valid and use the home image in |
| 7 | both scenes. |
| 8 | |
| 9 | > Chinese: [THEME_PACK.zh-CN.md](./THEME_PACK.zh-CN.md) |
| 10 | |
| 11 | ## Goals (first release) |
| 12 | |
| 13 | - Built-in styles, user themes, backgrounds, live preview, import/export, local library |
| 14 | - Full background on the home (empty) scene; reduced opacity + directional overlay on task scenes |
| 15 | - Works with Classic / Workbench / Creation and `auto` / `light` / `dark` |
| 16 | - **No** online marketplace, cloud sync, or script plugins |
| 17 | |
| 18 | ## Theme experience (settings IA) |
| 19 | |
| 20 | Appearance is split into two surfaces (no third entry): |
| 21 | |
| 22 | 1. **Appearance overview** — current theme summary, light/dark mode, **one** base-style |
| 23 | control, fonts and zoom. Primary action: **Browse themes**. |
| 24 | 2. **Theme gallery** — official / my themes / base styles tabs, select-to-inspect cards, |
| 25 | detail panel with isolated preview, temporary full-app preview, and a single |
| 26 | **Apply theme** action. Immersive preview is part of the gallery detail flow. |
| 27 | |
| 28 | State model (schema v2 of `desktop-theme-state.json`): |
| 29 | |
| 30 | | State | Meaning | Persistence | |
| 31 | | --- | --- | --- | |
| 32 | | `themeMode` | auto / light / dark | desktop config | |
| 33 | | `baseStyle` | Graphite…Amber | desktop config (`theme_style`) | |
| 34 | | `activeThemeId` | official, user or plugin pack only | `desktop-theme-state.json` | |
| 35 | | `selectedThemeId` / `previewThemeId` | gallery selection / temp preview | frontend memory only | |
| 36 | |
| 37 | - `activeThemeId` **must not** store base style ids. Choosing a base style clears the pack. |
| 38 | - Applying a pack keeps `baseStyle` as the disable/fallback value. |
| 39 | - Light/dark mode is independent of the pack. |
| 40 | |
| 41 | ## Theme kinds |
| 42 | |
| 43 | The gallery has four groups: |
| 44 | |
| 45 | | Kind | Source | Editable | Deletable | Exportable | |
| 46 | | --- | --- | --- | --- | --- | |
| 47 | | **Base styles** | Six visual directions (Graphite, Aurora, Slate, Carbon, Nocturne, Amber), token-less | no (duplicate first) | no | no | |
| 48 | | **Official themes** | Eight read-only packs embedded in the installer (manifest + original background + thumbnail, MIT) | no (duplicate first) | no | no | |
| 49 | | **User themes** | Created in the editor, duplicated, or imported as `.reasonix-theme` | yes | yes | yes | |
| 50 | | **Plugin themes** | `.reasonix-theme` packs contributed by enabled plugins (Manifest v1 `contributes.themes`), read straight from the plugin root — never copied into the user library | no | no (disable/uninstall the plugin) | no | |
| 51 | |
| 52 | - All 14 built-in ids (6 base + 8 official) are **reserved**: save, import, copy-over |
| 53 | and delete all refuse collisions. |
| 54 | - Activating an official theme stores only its id in `desktop-theme-state.json` — |
| 55 | assets are read from the embedded copy at runtime. |
| 56 | - "Duplicate" on a base/official theme creates an ordinary editable user theme |
| 57 | (the official background is copied into the user library); the duplicate can |
| 58 | then be edited or exported. |
| 59 | - v1 states that stored a base id as `activeThemeId` are migrated to `desktop.theme_style` |
| 60 | and cleared on load. |
| 61 | - Plugin theme ids are external names of the form `plugin:<plugin>:<theme>`; the |
| 62 | pack's own `id` keeps following the usual id rules. Invalid contributed files |
| 63 | are skipped with a warning in the theme views, never fatal. When the plugin |
| 64 | behind the active id is missing, disabled or uninstalled, rendering falls back |
| 65 | to the configured base style but the id is **preserved** in |
| 66 | `desktop-theme-state.json` — reinstalling the same plugin restores the theme. |
| 67 | Save/delete/duplicate/export reject plugin theme ids as read-only. |
| 68 | |
| 69 | ### The eight official themes |
| 70 | |
| 71 | | ID | Name | Base style | Artwork | |
| 72 | | --- | --- | --- | --- | |
| 73 | | `official-rose-dawn` | Rose Dawn / 玫瑰晨光 | graphite | Ivory dawn, soft roses, original illustrated muse | |
| 74 | | `official-fortune-forge` | Fortune Forge / 鸿运工坊 | amber | Vermilion/gold/jade workshop, original lucky programmer | |
| 75 | | `official-crimson-horizon` | Crimson Horizon / 赤曜新城 | graphite | Coral-red future city skyline, no people | |
| 76 | | `official-sage-breeze` | Sage Breeze / 鼠尾草清风 | slate | Cream paper, sage sprigs, original reader | |
| 77 | | `official-spark-notebook` | Spark Notebook / 灵感手账 | aurora | Notebook grid with stationery, original anime adult | |
| 78 | | `official-violet-starlight` | Violet Starlight / 紫曜星夜 | nocturne | Blue-violet starfield, butterflies, silhouette muse | |
| 79 | | `official-cyan-stage` | Cyan Stage / 青岚舞台 | carbon | Cyan stage, light rings, original digital performer | |
| 80 | | `official-noir-gold` | Noir Gold / 黑金序曲 | carbon | Black velvet, gold spotlights, original gentleman | |
| 81 | |
| 82 | Previews are shown inside the app's theme library (Settings → Appearance) from |
| 83 | real Reasonix builds. **Screenshots of the app must not be imported as theme |
| 84 | backgrounds.** Asset provenance, hashes and licence ledger: |
| 85 | [THEME_ASSETS.md](./THEME_ASSETS.md) · generator scripts in |
| 86 | `scripts/official-theme-art/` (procedural, fixed seeds, reproducible). |
| 87 | |
| 88 | ## Package format |
| 89 | |
| 90 | Distribute as a `.reasonix-theme` ZIP. The archive root may contain **only**: |
| 91 | |
| 92 | | File | Required | Notes | |
| 93 | | --- | --- | --- | |
| 94 | | `theme.json` | yes | Manifest (≤ 1 MiB) | |
| 95 | | `background.png` / `.jpg` / `.jpeg` / `.webp` | no | Home image ≤ 16 MiB, ≤ 8192×8192 | |
| 96 | | `background-task.png` / `.jpg` / `.jpeg` / `.webp` | no | Independent task/workspace image ≤ 16 MiB, ≤ 8192×8192 (V2) | |
| 97 | |
| 98 | ZIP limits: package ≤ 36 MiB; no nested directories, no symlinks, no duplicate entries, no path traversal. |
| 99 | |
| 100 | ### `theme.json` example |
| 101 | |
| 102 | ```json |
| 103 | { |
| 104 | "schemaVersion": 2, |
| 105 | "id": "my-theme", |
| 106 | "name": "My Theme", |
| 107 | "author": "", |
| 108 | "description": "", |
| 109 | "license": "", |
| 110 | "baseStyle": "graphite", |
| 111 | "tokens": { |
| 112 | "light": { |
| 113 | "bg": "#f4f3ef", |
| 114 | "fg": "#111827", |
| 115 | "accent": "#2f5fa8" |
| 116 | }, |
| 117 | "dark": { |
| 118 | "bg": "#0c0d10", |
| 119 | "fg": "#f1f1ef", |
| 120 | "accent": "#ff6a3d" |
| 121 | } |
| 122 | }, |
| 123 | "recipes": { |
| 124 | "density": "comfortable", |
| 125 | "corners": "soft" |
| 126 | }, |
| 127 | "background": { |
| 128 | "image": "background.webp", |
| 129 | "focusX": 0.72, |
| 130 | "focusY": 0.45, |
| 131 | "safeArea": "left", |
| 132 | "homeOpacity": 1, |
| 133 | "taskOpacity": 0.28, |
| 134 | "overlayStrength": 0.62 |
| 135 | }, |
| 136 | "taskBackground": { |
| 137 | "image": "background-task.webp", |
| 138 | "focusX": 0.5, |
| 139 | "focusY": 0.5, |
| 140 | "safeArea": "right", |
| 141 | "opacity": 0.28, |
| 142 | "overlayStrength": 0.62 |
| 143 | } |
| 144 | } |
| 145 | ``` |
| 146 | |
| 147 | JSON Schema: [theme-pack.schema.json](./theme-pack.schema.json) |
| 148 | |
| 149 | ### Fields |
| 150 | |
| 151 | | Field | Rules | |
| 152 | | --- | --- | |
| 153 | | `schemaVersion` | `1` or `2`; `taskBackground` requires `2` | |
| 154 | | `id` | Lowercase `[a-z][a-z0-9-]*`, reserved: `graphite`, `aurora`, `slate`, `carbon`, `nocturne`, `amber` | |
| 155 | | `baseStyle` | One of the six built-in directions; uncovered tokens inherit it | |
| 156 | | `tokens.light` / `tokens.dark` | Optional maps of semantic keys → `#RRGGBB` or `#RRGGBBAA` only | |
| 157 | | `recipes.density` | `compact` \| `comfortable` | |
| 158 | | `recipes.corners` | `square` \| `soft` \| `round` | |
| 159 | | `background.image` | Bare file name only (png/jpeg/webp) | |
| 160 | | `background.focusX/Y` | 0–1 focal point | |
| 161 | | `background.safeArea` | `left` \| `right` \| `center` (task overlay direction) | |
| 162 | | `background.homeOpacity` | 0–1 | |
| 163 | | `background.taskOpacity` | 0–1 | |
| 164 | | `background.overlayStrength` | 0–1 | |
| 165 | | `background.paneOpacity` | 0–1 (home scene panel opacity) | |
| 166 | | `taskBackground.image` | Optional independent task/workspace image; bare local file name only | |
| 167 | | `taskBackground.focusX/Y` | 0–1 focal point | |
| 168 | | `taskBackground.safeArea` | `left` \| `right` \| `center` | |
| 169 | | `taskBackground.opacity` | 0–1 | |
| 170 | | `taskBackground.overlayStrength` | 0–1 | |
| 171 | | `taskBackground.paneOpacity` | 0–1 (task scene panel opacity) | |
| 172 | |
| 173 | ### Allowed token keys |
| 174 | |
| 175 | `bg`, `bgSoft`, `bgElev`, `panel`, `sidebar`, `chat`, `workspace`, `workspaceFiles`, |
| 176 | `border`, `borderSoft`, `fg`, `fgDim`, `fgFaint`, `accent`, `accentFg`, `ok`, `warn`, `err` |
| 177 | |
| 178 | Colors must **not** include `url()`, gradients, or arbitrary CSS. |
| 179 | |
| 180 | ## Engine behavior |
| 181 | |
| 182 | 1. Apply global `auto` / `light` / `dark` and the base visual style. |
| 183 | 2. Apply the pack overlay (CSS custom properties) **after** stylesheets so it wins over trailing `:root` and Creation locals. |
| 184 | 3. Root gets `data-theme-pack="<id>"`; the app container gets `data-theme-scene="home|task"`. |
| 185 | 4. Scene is derived only from whether the current session has content — it does not change chat lifecycle. |
| 186 | 5. Background is a fixed, non-interactive layer. Task scene dims the image and paints a directional wash (**no** `backdrop-filter`). |
| 187 | |
| 188 | ## Storage |
| 189 | |
| 190 | | Path under Reasonix home | Purpose | |
| 191 | | --- | --- | |
| 192 | | `desktop-theme-state.json` | Versioned active theme pointer (not `config.toml`) | |
| 193 | | `themes/<id>/` | User theme library (`theme.json` + up to two optional scene images) | |
| 194 | |
| 195 | Legacy installs without theme state keep the previous appearance. Old app versions ignore the new directory. CLI theme, prompts, provider requests, and cache keys are unchanged. |
| 196 | |
| 197 | ## Desktop bridge (frontend) |
| 198 | |
| 199 | List / activate / reset / save / delete / copy / import / export / pick background. |
| 200 | The UI only receives temporary asset URLs (`/__reasonix_theme_asset/...`) or data URLs — never absolute host paths. |
| 201 | |
| 202 | Import: same id is rejected until the user confirms atomic replace. Built-ins cannot be overwritten or deleted. Corrupt / missing packs fall back to the Graphite path. `/theme reset` and the command palette restore entry clear the pack. |
| 203 | |
| 204 | ## Authoring tips |
| 205 | |
| 206 | 1. Start from a built-in direction and override only the tokens you need. |
| 207 | 2. Prefer WCAG AA contrast (≈ 4.5:1 body text). The editor warns but does not block save. |
| 208 | 3. Before sharing a pack with a photo or portrait, confirm redistribution rights. |
| 209 | 4. Do not ship third-party or copyrighted reference assets from other products. |
| 210 | |
| 211 | ## Template |
| 212 | |
| 213 | A minimal, royalty-free starter (no portrait photos): |
| 214 | |
| 215 | ```json |
| 216 | { |
| 217 | "schemaVersion": 1, |
| 218 | "id": "paper-dawn", |
| 219 | "name": "Paper Dawn", |
| 220 | "author": "Reasonix", |
| 221 | "description": "Template theme — solid tokens only, no background image.", |
| 222 | "license": "CC0-1.0", |
| 223 | "baseStyle": "graphite", |
| 224 | "tokens": { |
| 225 | "light": { |
| 226 | "bg": "#f7f4ef", |
| 227 | "panel": "#ffffff", |
| 228 | "sidebar": "#f3efe8", |
| 229 | "chat": "#fbfaf7", |
| 230 | "fg": "#1c1917", |
| 231 | "fgDim": "#57534e", |
| 232 | "fgFaint": "#a8a29e", |
| 233 | "border": "#e7e5e4", |
| 234 | "accent": "#c2410c", |
| 235 | "accentFg": "#fff7ed", |
| 236 | "ok": "#15803d", |
| 237 | "warn": "#b45309", |
| 238 | "err": "#b91c1c" |
| 239 | }, |
| 240 | "dark": { |
| 241 | "bg": "#0c0b0a", |
| 242 | "panel": "#171412", |
| 243 | "sidebar": "#141210", |
| 244 | "chat": "#0c0b0a", |
| 245 | "fg": "#f5f5f4", |
| 246 | "fgDim": "#a8a29e", |
| 247 | "fgFaint": "#78716c", |
| 248 | "border": "#292524", |
| 249 | "accent": "#fb923c", |
| 250 | "accentFg": "#0c0b0a", |
| 251 | "ok": "#4ade80", |
| 252 | "warn": "#fbbf24", |
| 253 | "err": "#f87171" |
| 254 | } |
| 255 | }, |
| 256 | "recipes": { |
| 257 | "density": "comfortable", |
| 258 | "corners": "soft" |
| 259 | } |
| 260 | } |
| 261 | ``` |
| 262 | |
| 263 | Zip as `paper-dawn.reasonix-theme` with only `theme.json` at the root. |
| 264 |