# Theme migration plan: full migration to the Zeron theme model
Full migration of `crates/theme` (currently derived from [zed-industries/zed `crates/theme`](https://github.com/zed-industries/zed/tree/main/crates/theme)) to the architecture of [zeronsh/zeron `crates/theme`](https://github.com/zeronsh/zeron/tree/main/crates/theme).
**Scope decision (v2):** wholesale migration. We do *not* preserve the current brand look, do *not* keep the old ~70-role UI color vocabulary, and do *not* bridge the old JSON theme assets. Zeron's model, role set, builtins, and defaults become coop's. A branding refresh (custom coop family/seeds) happens later, on top of the new model.
- Coop license: GPLv3 (theme crate currently credits Zed).
- Zeron license: MIT — code may be ported into coop with attribution (add zeron to `crates/theme/README.md` credits, record the ported revision).
- Preferences: `crates/workspace/src/dialogs/settings.rs` — Light/Dark toggle via `Theme::change`.
- `sync_base()` mirrors coop colors into the `gpui-base` global theme (focus ring, selection, scrollbars, overlays, typography).
### Usage scale (defines the rewrite scope)
- `use theme::ActiveTheme` across 7 crates.
- ~370 `cx.theme().*` references in **51 files**: `ui` (23), `workspace` (22), `chat_ui` (1, one large `lib.rs`), `nip29_ui` (4), `nip4e` (1). ~305 are color roles; the rest are `radius` (41), `shadow` (10), `radius_lg` (8), `notification` (2), `font_*` (2) — these app-chrome fields are unchanged by the migration.
- 55 distinct color roles are referenced; `ThemeColors` is never named outside `crates/theme`.
---
## 2. Target state (zeron `crates/theme`)
A **source-neutral, gpui-free domain model** plus an offline import pipeline:
| Color type | `gpui::Hsla` everywhere | Own `Color` (rgba u8) with WCAG contrast math; converted to `Hsla` only at the gpui boundary |
| Family shape | `{ light: ThemeColors, dark: ThemeColors }` pair | `variants: Vec<ThemeVariant>`, each variant carries its own appearance + full payload |
- **D1 — Two layers, one crate.**`crates/theme` gets a gpui-free **domain** module (port of zeron's model, kept faithful so upstream diffs stay readable) and a gpui **runtime** module (coop's `Theme` global, `ActiveTheme`, `sync_base`, app chrome). No new crate.
- **D2 — Zeron's role vocabulary is the only UI color vocabulary.** The old ~70-role `ThemeColors` is deleted. UI code uses the 28 zeron roles + `AccentRoles` exclusively. All 305 color call sites are rewritten (Phase 3) using the mapping in §7.
- **D3 — Zeron builtins and defaults adopted as-is.** Port all 19 families / 30 variants unchanged. Default selection: `ThemeSelection::default()` = `zeron-light`/`zeron-dark`. `assets/themes/*.json` and the asset-loading registry are deleted. A coop-branded family is a later, separate change (swap seeds, keep the model).
- **D4 — Runtime exposes a resolved `Hsla` snapshot.** The gpui `Theme` global stores `variant: Arc<ThemeVariant>`, resolved `appearance`, `accent: AccentRoles`, `surface: SurfaceTreatment`, and a precomputed snapshot with the *same field names* as the domain `ThemeColors`/`AccentRoles` but typed `Hsla` (`ResolvedColors`, `ResolvedAccentRoles`). Call sites stay ergonomic (`cx.theme().shell`, `cx.theme().accent.primary`) and gpui-native (`.alpha(...)` still works). App-chrome fields (`font_family`, `font_size`, `radius`, `radius_lg`, `shadow`, `scrollbar_mode`, `notification`) remain on `Theme`, unchanged.
- **D5 — Settings.**`theme: Option<String>` + `theme_mode: ThemeMode` → `theme_selection: ThemeSelection` + appearance setting; old keys are dropped on load (`theme_mode` maps trivially to `Appearance`; the old theme path is reset to defaults — the ids changed anyway). New settings: `accent_selection: AccentSelection`, `surface_preference: SurfacePreference`. Startup keeps following the OS appearance via `sync_system_appearance`.
- **D6 — Transitional compile scaffold, not an end state.** To keep every commit building while 51 files are rewritten, Phase 2 temporarily provides the removed role names (`ghost_element_hover`, `element_background`, `tab_*`, …) as fields on the resolved snapshot, computed from zeron roles per §7. They carry a doc comment "transitional — delete in Phase 5" and are removed as soon as call-site migration completes. This is a tactic, not a compat layer: no visual parity is promised.
- **D7 — Scope cuts for v1.** No frosted rendering (enums land; resolution returns `Opaque`; gpui blur later). `TerminalPalette`/`syntax` are carried in the model with no consumer yet (future: code-block highlighting). `VisualFixture` is not ported. The import CLI (`bin/`) is not ported.
- **D8 — Web target.**`library.rs`/`vscode.rs` use `std::fs`; gate them behind `#[cfg(not(target_arch = "wasm32"))]` (or a cargo feature) so `coop_web` keeps building, mirroring how `settings` gates file IO.
└── vscode.rs # Phase 6 (optional): VS Code import
```
Deleted: `src/colors.rs`, `src/scale.rs`, `src/theme.rs`, `src/registry.rs`, `assets/themes/*`, the `themes` include in `crates/assets`, and the `schemars` dependency.
---
## 6. Implementation phases
Each phase leaves the workspace building and tested (D6 makes this possible during the rewrite). Commit boundaries follow phases.
### Phase 0 — Prep (S)
1. Record the ported zeron revision (commit hash) for future diffing; add MIT attribution to `crates/theme/README.md`.
1. `crates/theme/Cargo.toml`: `+ thiserror`, `+ sha2` (both workspace or pinned).
2. Create `src/model.rs` — faithful port of zeron's `lib.rs` (§2), keeping its *why* doc comments (provenance hashing, severity semantics, …) and its test module (color round-trip, accent contrast, surface resolution, builtin validation/counts).
3. Create `src/builtins.rs` — faithful port of the `Seeds` machinery and all 19 families (derived roles, syntax map, ANSI palettes, sha256 self-hash), including the registry-count tests.
4. Export from `lib.rs` without wiring anything into the app yet. Project rules: `Result` propagation, no `unwrap()`; `expect` only for constant builtin seed colors (statically covered by the builtin validation test, as upstream).
**Acceptance:** `cargo test -p theme` green with ported tests; `ThemeRegistry::builtin().validate()` reports no blocking issues; app behavior unchanged.
### Phase 2 — Runtime rework (L)
1. Add `src/resolved.rs`: `ResolvedColors` (28 fields, `Hsla`) and `ResolvedAccentRoles` (primary/strong/wash/on/selection/caret/activity/glyph, `Hsla`), built from a `ThemeVariant` + resolved `AccentRoles` at theme-change time (cheap, precomputed). Conversion: zeron `Color { r, g, b, a }` → `gpui::Rgba` → `Hsla`.
2. Add `src/runtime.rs` and rewrite `lib.rs` around it:
- `Theme` global fields: `variant: Arc<ThemeVariant>`, `appearance: Appearance`, `accent: AccentRoles`, `surface: SurfaceTreatment`, `colors: ResolvedColors` (plus `Deref<Target = ResolvedColors>`), and the unchanged app-chrome fields;
- `Theme::change(appearance)`: resolve `ThemeRegistry::active().resolve(&selection, appearance)` → rebuild the snapshot; still falls back to zeron defaults;
- `sync_system_appearance` / `sync_scrollbar_appearance` keep their roles;
- `ActiveTheme` unchanged; `ThemeMode` deleted, replaced by `Appearance` (`From<WindowAppearance>` ported).
3. Add the **transitional scaffold** fields to `ResolvedColors` (D6): the removed role names computed from zeron roles per §7, e.g. `surface_background = shell`, `ghost_element_hover = hover`, `element_background = accent.primary`. All 55 used roles covered so downstream compiles untouched.
4. Delete `src/colors.rs`, `src/scale.rs`, `src/theme.rs`, `src/registry.rs`; delete `assets/themes/*.json` and the `themes` include in `crates/assets/src/lib.rs`; remove `schemars` from `Cargo.toml`.
5. Settings (`crates/settings/src/lib.rs`): swap persisted fields to `theme_selection` (+ `#[serde(default)]``accent_selection`, `surface_preference`), map old `theme_mode` → appearance on load, drop the old `theme` key; `apply_theme` resolves through the registry (`Theme::change`). Update `desktop`/`web` init if signatures moved.
6. Rewrite `sync_base()` per §9.
**Acceptance:** app builds and runs on zeron defaults; appearance toggle + OS sync work; settings round-trip and the old `.settings` file loads (theme resets to defaults, mode preserved); no downstream crate has been touched yet.
3. `chat_ui` (1 large file), `nip29_ui` (4), `nip4e` (1).
4. Mechanics: apply the §7 mapping mechanically first, then review each file for roles that encode *intent* (e.g. `secondary_*` used as chip styling → `accent.wash` vs `raised`) and for contrast-sensitive spots (text on tinted backgrounds → `ensure_contrast` via the resolved accent where needed).
**Acceptance:** `grep -rE 'theme\(\)\.(element_|secondary_|ghost_element_|tab_|scrollbar_thumb|title_bar|panel_background|surface_background|elevated_surface)' crates` returns nothing; each crate compiles + its tests pass before moving on.
### Phase 4 — Theme UI renewal (M)
1. `theme_selector()`: list `ThemeRegistry::active().variants_for(appearance)` grouped by family; show variant name, family, provenance (url/license); selecting writes `theme_selection.set_variant(appearance, id)` and applies.
2. Preferences dialog (`workspace/dialogs/settings.rs`): Light/Dark toggle now `Appearance`; add **Accent** picker (`AccentSelection`: Theme default + `AccentPreset::ALL`) and **Surface** preference; changing accent re-resolves `AccentRoles` and refreshes windows.
2. Gate file IO for the web target (D8); library lives under `common::config_dir()` (`theme-library.json`, `custom-themes/*.zeron-theme.json`).
3. UI: "Import theme…" flow (file picker → `compile_source` → report dialog with mappings/adjustments/validation) and a library manager (reload/unlink/duplicate/remove). Errors surface in the UI per project rules.
4. After library load/mutation: `install_runtime()` → re-resolve the active theme.
**Acceptance:** ported zeron test suites pass; importing a VS Code package yields usable variants with a report; broken sources keep the last known good theme; `coop_web` unaffected by the gated IO.
### Phase 7 — Validation, tests, release (M)
1. Add a runtime smoke test: resolving every builtin variant produces a `ResolvedColors` snapshot with finite/valid values, and `Theme::change` round-trips appearances.
2. Settings migration test (old `.settings` → new fields).
3. Full QA pass on desktop (+ web if Phase 6 landed): §11 checklist; spot-check a handful of variants (zeron light/dark, catppuccin mocha/latte, github dark).
4. Release notes; schedule the deferred branding pass (custom coop family seeds) as follow-up work.
---
## 7. Call-site rewrite mapping (old role → zeron vocabulary)
`accent.*` refers to the resolved `AccentRoles`. Where intent matters more than mechanics, prefer the *Intent* column over the mechanical mapping.
- `Ghost { alt }` → transparent / `raised` bg; `hover`/`active` states from domain roles.
- `Transparent` → `text_faint` fg.
- **Tabs** (`tab/`): inactive on `shell` with `text_muted`; hover `hover`; active merges to `background` with `text`; accent indicator via `accent.primary`.
- **Precomputed derived fields** that several components need (`primary_hover`, `primary_active`, `accent_text_on_wash`, `onDanger`…) belong on `ResolvedAccentRoles` — computed once at resolve time with `ensure_contrast`, not ad-hoc at call sites.
- On load: `theme_mode` maps to `Appearance`; the old `theme` path is ignored (ids changed; optional nicety: match `catppuccin-mocha`-style family names to builtin ids). Old keys are dropped on next save.
| `crates/assets` | remove the `themes/**/*` include |
| others | none |
## 12. Risks & open questions
- **Call-site drift:** mechanical mapping can flatten intent (e.g. everything "muted" becoming `text_faint`). Mitigated by the per-file review pass in Phase 3.4 and precomputed contrast-safe fields (§8).
- **Big-bang compile risk:** contained by the D6 scaffold — every intermediate commit builds.
- **Visual QA:** defaults change to zeron's look; accepted by scope. Spot-check a handful of variants; full polish belongs to the later branding pass.
- **Frosted surfaces:** enums land now, rendering later; the preferences UI should not advertise a visible no-op (disable/annotate the control until rendering exists).
- **`Solid` vs accent for primary buttons:** §8 defaults to accent; if the neutral look wins, switch `Primary` to `solid`/`on_solid` — one place.
- **Zeron upstream drift:** record the ported revision; keep the port faithful to ease future diffing.
- **Web target:** Phase 6 file IO must stay wasm-gated; verify `coop_web` builds in CI from Phase 2 onward.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.