diff --git a/crates/theme/LICENSE b/crates/theme/LICENSE deleted file mode 100644 index fd50777c..00000000 --- a/crates/theme/LICENSE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE diff --git a/crates/theme/README.md b/crates/theme/README.md deleted file mode 100644 index c5de815c..00000000 --- a/crates/theme/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# theme - -Theme model and runtime for coop. - -The domain model, built-in theme families, and registry design are ported from -[zeron/theme](https://github.com/zeronsh/zeron/tree/main/crates/theme) (MIT licensed), -ported at revision `916cb1ccb1342c2061963a5f6ecf5c977ba46f2c`. The original -gpui-integrated implementation this crate replaced was based on -[zed/theme](https://github.com/zed-industries/zed/tree/main/crates/theme) -(AGPL/GPL, see LICENSE-GPL in the zed repository). - -CREDITS: [zeron/theme](https://github.com/zeronsh/zeron/tree/main/crates/theme), -[zed/theme](https://github.com/zed-industries/zed/tree/main/crates/theme) diff --git a/crates/theme/src/builtins.rs b/crates/theme/src/builtins.rs index 089a4bed..7bfbdd04 100644 --- a/crates/theme/src/builtins.rs +++ b/crates/theme/src/builtins.rs @@ -12,7 +12,7 @@ pub fn builtin_registry() -> &'static ThemeRegistry { static REGISTRY: OnceLock = OnceLock::new(); REGISTRY.get_or_init(|| ThemeRegistry { families: vec![ - family("zeron", "Zeron", vec![zeron_light(), zeron_dark()]), + family("coop", "Coop", vec![coop_light(), coop_dark()]), family("dracula", "Dracula", vec![dracula()]), family("github", "GitHub", vec![github_light(), github_dark()]), family("ayu", "Ayu", vec![ayu_light(), ayu_dark(), ayu_mirage()]), @@ -119,7 +119,7 @@ fn variant(seed: Seeds<'_>) -> ThemeVariant { raised, card, dialog: card.mix(raised, if dark { 0.18 } else { 0.04 }), - overlay: card.mix(raised, if dark { 0.34 } else { 0.02 }), + overlay: Color::BLACK.with_alpha(if dark { 0.5 } else { 0.3 }), hover: border_tone.with_alpha(if dark { 0.11 } else { 0.06 }), active: accent.primary.with_alpha(if dark { 0.18 } else { 0.10 }), border: border_tone.with_alpha(if dark { 0.10 } else { 0.12 }), @@ -159,9 +159,6 @@ fn variant(seed: Seeds<'_>) -> ThemeVariant { }, source: seed.source, }; - // Hash the checked-in resolved definition itself (with the hash field - // blanked), not merely its source URL. This makes provenance sensitive to - // curation edits as well as upstream revision changes. variant.source.asset_hash.clear(); let encoded = serde_json::to_vec(&variant).expect("built-in theme serializes"); variant.source.asset_hash = format!("sha256:{:x}", Sha256::digest(encoded)); @@ -236,11 +233,11 @@ const ANSI_LIGHT: [&str; 16] = [ "#71717a", "#b91c1c", "#15803d", "#92400e", "#1d4ed8", "#7e22ce", "#155e75", "#18181b", ]; -fn zeron_dark() -> ThemeVariant { +fn coop_dark() -> ThemeVariant { variant(Seeds { - id: "zeron-dark", - family_id: "zeron", - name: "Zeron Dark", + id: "coop-dark", + family_id: "coop", + name: "Coop Dark", appearance: Appearance::Dark, treatment: SurfaceTreatment::Frosted, background: "#060606", @@ -250,7 +247,7 @@ fn zeron_dark() -> ThemeVariant { text: "#e8e8ea", muted: "#a9a9ae", faint: "#85858a", - accent: "#8b7cf6", + accent: "#FFE629", danger: "#f87171", warning: "#facc15", success: "#34d399", @@ -261,20 +258,20 @@ fn zeron_dark() -> ThemeVariant { "#a1a1aa", "#f472b6", "#22d3ee", "#f87171", ], source: source( - "zeron-dark", + "coop-dark", "native", - "https://github.com/zeronsh/comet", + "https://git.reya.info/reya/coop", "d138049", "MIT", ), }) } -fn zeron_light() -> ThemeVariant { +fn coop_light() -> ThemeVariant { variant(Seeds { - id: "zeron-light", - family_id: "zeron", - name: "Zeron Light", + id: "coop-light", + family_id: "coop", + name: "Coop Light", appearance: Appearance::Light, treatment: SurfaceTreatment::Frosted, background: "#ffffff", @@ -284,7 +281,7 @@ fn zeron_light() -> ThemeVariant { text: "#303035", muted: "#62626a", faint: "#797981", - accent: "#5b43e8", + accent: "#a19200", danger: "#dc2626", warning: "#a16207", success: "#15803d", @@ -295,9 +292,9 @@ fn zeron_light() -> ThemeVariant { "#52525b", "#be185d", "#0e7490", "#b91c1c", ], source: source( - "zeron-light", + "coop-light", "native", - "https://github.com/zeronsh/comet", + "https://git.reya.info/reya/coop", "d138049", "MIT", ), diff --git a/crates/theme/src/model.rs b/crates/theme/src/model.rs index f7a054bb..f615e479 100644 --- a/crates/theme/src/model.rs +++ b/crates/theme/src/model.rs @@ -240,7 +240,8 @@ pub struct ColorParseError; #[serde(rename_all = "camelCase")] pub enum AccentPreset { #[default] - Zeron, + #[serde(alias = "coop")] + Coop, Orange, Amber, Green, @@ -251,7 +252,7 @@ pub enum AccentPreset { impl AccentPreset { pub const ALL: [Self; 7] = [ - Self::Zeron, + Self::Coop, Self::Orange, Self::Amber, Self::Green, @@ -262,7 +263,7 @@ impl AccentPreset { pub fn label(self) -> &'static str { match self { - Self::Zeron => "Zeron", + Self::Coop => "Coop", Self::Orange => "Orange", Self::Amber => "Amber", Self::Green => "Green", @@ -274,7 +275,7 @@ impl AccentPreset { pub fn color(self, appearance: Appearance) -> Color { let (dark, light) = match self { - Self::Zeron => ("#8b7cf6", "#5b43e8"), + Self::Coop => ("#FFE629", "#a19200"), Self::Orange => ("#fb923c", "#c2410c"), Self::Amber => ("#fbbf24", "#a16207"), Self::Green => ("#4ade80", "#15803d"), @@ -357,8 +358,8 @@ pub struct ThemeSelection { impl Default for ThemeSelection { fn default() -> Self { Self { - light: "zeron-light".into(), - dark: "zeron-dark".into(), + light: "coop-light".into(), + dark: "coop-dark".into(), } } } @@ -489,12 +490,12 @@ impl ThemeRegistry { self.variant(selection.variant_id(appearance)) .or_else(|| { self.variant(if appearance.is_dark() { - "zeron-dark" + "coop-dark" } else { - "zeron-light" + "coop-light" }) }) - .expect("the built-in registry always contains both Zeron variants") + .expect("the built-in registry always contains both Coop variants") } pub fn validate(&self) -> Vec { @@ -711,8 +712,8 @@ mod tests { .sum::(), 30 ); - assert!(registry.variant("zeron-light").is_some()); - assert!(registry.variant("zeron-dark").is_some()); + assert!(registry.variant("coop-light").is_some()); + assert!(registry.variant("coop-dark").is_some()); let errors: Vec<_> = registry .validate() .into_iter() diff --git a/crates/ui/src/dialog.rs b/crates/ui/src/dialog.rs index 4be3f73a..d5c3c764 100644 --- a/crates/ui/src/dialog.rs +++ b/crates/ui/src/dialog.rs @@ -341,7 +341,7 @@ impl RenderOnce for Dialog { .id(layer_ix) .bg(cx.theme().background) .border_1() - .border_color(cx.theme().border.alpha(0.4)) + .border_color(cx.theme().border) .rounded(radius) .when(cx.theme().shadow, |this| this.shadow_xl()) .min_h_24() diff --git a/docs/THEME_MIGRATION.md b/docs/THEME_MIGRATION.md deleted file mode 100644 index d77e9288..00000000 --- a/docs/THEME_MIGRATION.md +++ /dev/null @@ -1,324 +0,0 @@ -# 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). - ---- - -## 1. Current state (coop) - -### Crate layout (`crates/theme`) - -| File | Contents | -| --- | --- | -| `src/lib.rs` | `Theme` gpui `Global` (colors + mode + font/radius/shadow/scrollbar/notification), `ActiveTheme` trait, `init()`, `sync_base()` projection into `gpui-base` tokens, layout constants (`TITLEBAR_HEIGHT`, …) | -| `src/theme.rs` | `ThemeMode` (Light/Dark), `ThemeFamily { id, name, author, url, light: ThemeColors, dark: ThemeColors }`, `from_file`/`from_assets` JSON loading, tests | -| `src/colors.rs` | `ThemeColors`: ~70 `Hsla` roles (Zed-style: `element_*`, `secondary_*`, `danger_*`, `warning_*`, `ghost_element_*`, `tab_*`, `scrollbar_*`, …) with `light()`/`dark()` defaults built from color scales | -| `src/scale.rs` | Zed `ColorScale`/`ColorScaleStep`/`ColorScaleSet` (12-step scales) | -| `src/registry.rs` | `ThemeRegistry`: gpui `Entity` global; loads `themes/**` from the `AssetSource` into `HashMap>` | -| `src/geometry.rs`, `src/notification.rs`, `src/scrollbar_mode.rs`, `src/platform_kind.rs` | App chrome: `Edges`/`Side`/`AxisExt`, `NotificationSettings`, `ScrollbarMode`, `PlatformKind` | - -### Assets & consumers - -- `assets/themes/*.json` — 8 theme family files (Zed family format): `aurora`, `catppuccin-{latte,frappe,macchiato,mocha}`, `flexoki`, `forest`, `ocean`; embedded via `crates/assets` (rust-embed). -- Apps init: `desktop/src/main.rs` and `web/src/lib.rs` call `ui::init(cx)` then `theme::init(cx)`. -- Settings (`crates/settings/src/lib.rs`): persists `theme: Option` (asset path) + `theme_mode: ThemeMode` to `config_dir()/.settings` (JSON via `smol::fs`, wasm-gated); `AppSettings::apply_theme` loads `ThemeFamily::from_assets(path)` → `Theme::apply_theme` + `Theme::change`. -- Theme picker: `crates/workspace/src/lib.rs` `theme_selector()` lists `ThemeRegistry` entries (name, author, url link). -- 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: - -| File | Contents | -| --- | --- | -| `src/lib.rs` | Domain model: `Appearance`, `Color` (rgba u8, hex serde, `contrast`/`mix`/`blend_over`/`ensure_contrast`/`best_on_color`), `SurfaceTreatment`/`SurfacePreference`, `AccentPreset`/`AccentSelection`/`AccentRoles` (primary/strong/wash/on/selection/caret/activity/glyph[3], derived with WCAG hardening), `ThemeSelection { light, dark }`, `ThemeSource` (provenance + sha256 `asset_hash`), `ThemeColors` (28 roles: background, shell, raised, card, dialog, overlay, hover, active, border, border_strong, text, text_muted, text_faint, solid, on_solid, danger(+_muted), warning(+_muted), success(+_muted), input, cursor, diff_add/delete/hunk), `TerminalPalette` (bg/fg/selection + ansi[16]), `ThemeVariant` (id, family_id, name, appearance, recommended_surface_treatment, colors, accent, syntax map, terminal, source), `ThemeFamily { id, name, variants }`, `ThemeRegistry` (`builtin()`/`active()`/`variant()`/`variants_for()`/`resolve()`/`validate()`), `ValidationIssue`/`Severity`/`Category`, `VisualFixture`, `replace_custom_families()` | -| `src/builtins.rs` | 19 families / 30 variants compiled in Rust from a `Seeds` struct; derived roles (border/hover/active alphas, dialog/overlay mixes, accent roles, 24-key syntax map, terminal palette); sha256 provenance of the resolved definition | -| `src/library.rs` | `CustomThemeLibrary` persisted as `theme-library.json` (16 MB / 256 entries / 1024 variants caps, atomic recoverable saves, backup recovery); sources: `ImportedSnapshot`, `LinkedFile`, `LinkedPackage`, `EditableFile`; install/reload/unlink/duplicate/remove with last-known-good semantics | -| `src/vscode.rs` | Offline VS Code import: JSONC (`json5`) + `include` resolution + external token files + TextMate `.tmTheme` (`plist`); workbench-key → zeron-role mapping tables; contrast hardening; `ImportReport`; extension-package compilation with per-variant failure isolation; security bounds (4 MB sources, include depth 32, ≤128 variants, path containment) | -| `src/bin/zeron-theme-import.rs` | CLI wrapper around `compile_source` | - -Dependencies: `anyhow`, `serde`, `serde_json`, `json5`, `plist`, `sha2`, `thiserror` (clap only for the CLI). **No gpui.** - ---- - -## 3. Key architectural differences - -| Aspect | Coop today (Zed-style) | Zeron target | -| --- | --- | --- | -| 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`, each variant carries its own appearance + full payload | -| Role count | ~70 UI roles | 28 semantic roles + derived `AccentRoles` | -| Accent | Implicit (`element_background`) | First-class: presets, per-variant `AccentRoles`, runtime `AccentSelection` override | -| Builtins | JSON files in `assets/themes`, loaded via `AssetSource` at runtime | Compiled into Rust (`builtins.rs`), sha256 provenance | -| Custom themes | None | Durable library (snapshot/link/editable) + VS Code import with reports & hardening | -| Validation | None | `validate()`: contrast thresholds, unique ids, provenance completeness; blocking vs. reviewable | -| Registry | gpui `Entity` global over a path map | Plain struct; `builtin()` static + process-wide custom families (`RwLock`) | -| Syntax / terminal | Not modeled | `syntax` map (24 keys) + `TerminalPalette` per variant | -| Surfaces | N/A | `SurfaceTreatment` (Opaque/Frosted) recommendation + user `SurfacePreference` | - ---- - -## 4. Guiding decisions - -- **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`, 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` + `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. - ---- - -## 5. Target crate layout - -``` -crates/theme/ -├── Cargo.toml # + thiserror, sha2 (json5/plist with Phase 6); − schemars -└── src/ - ├── lib.rs # re-exports; layout constants; init(); sync_base(); ActiveTheme (runtime) - ├── model.rs # zeron lib.rs port: Color, Appearance, Surface*, Accent*, - │ # ThemeSelection, ThemeSource, ThemeColors, TerminalPalette, - │ # ThemeVariant, ThemeFamily, ThemeRegistry, ValidationIssue (+ tests) - ├── builtins.rs # zeron builtins port: Seeds machinery + 19 families (+ tests) - ├── runtime.rs # Theme gpui global over the domain: resolve, change, apply_variant, - │ # sync_system_appearance, sync_scrollbar_appearance - ├── resolved.rs # ResolvedColors / ResolvedAccentRoles (Hsla mirror of the domain - │ # structs) + Color→Hsla conversion + transitional scaffold fields - ├── geometry.rs # unchanged (Edges/Side/AxisExt; used by NotificationSettings) - ├── notification.rs # unchanged - ├── scrollbar_mode.rs # unchanged - ├── platform_kind.rs # unchanged (font selection) - ├── library.rs # Phase 6 (optional): CustomThemeLibrary - └── 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`. -2. Baseline `cargo test` / `cargo build` state; confirm D7 scope cuts. -3. Create the migration branch. - -**Acceptance:** branch exists; decisions recorded. - -### Phase 1 — Port the zeron domain model (M) - -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`, `appearance: Appearance`, `accent: AccentRoles`, `surface: SurfaceTreatment`, `colors: ResolvedColors` (plus `Deref`), and the unchanged app-chrome fields; - - `Theme::change(appearance)`: resolve `ThemeRegistry::active().resolve(&selection, appearance)` → rebuild the snapshot; still falls back to zeron defaults; - - `Theme::apply_variant(variant)` replaces `apply_theme(family)`; - - `sync_system_appearance` / `sync_scrollbar_appearance` keep their roles; - - `ActiveTheme` unchanged; `ThemeMode` deleted, replaced by `Appearance` (`From` 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. - -### Phase 3 — Call-site migration: 51 files, 5 crates (L) - -Migrate in dependency order; after each crate the workspace builds and that crate uses only zeron vocabulary: - -1. `ui` (23 files) — includes the component redesigns of §8 (`button.rs` variants, `tab/`, `input/`, `menu/`, `dock/`, `root.rs`, `title_bar.rs`, …). -2. `workspace` (22 files) — sidebar/panels/dialogs + `theme_selector` placeholder compiles via scaffold. -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. -3. Register validation feedback (non-blocking `ValidationIssue`s) somewhere visible later — minimal v1: log only. - -**Acceptance:** picker lists families/variants filtered by appearance; selection persists per appearance; accent override recomputes accent-derived UI live; surface preference stored (rendering effect deferred per D7). - -### Phase 5 — Remove the scaffold & dead code (S) - -1. Delete every transitional field from `ResolvedColors` and the §7 scaffold computations. -2. Remove leftover APIs (`from_assets`, old tests/docs references), run `cargo clippy` + `cargo test` workspace-wide. -3. Update `crates/theme/README.md` (Zed + Zeron credits, ported revision). - -**Acceptance:** no scaffold remains; `cargo build` (desktop) and `cargo build -p coop_web` both green. - -### Phase 6 — Custom theme library + VS Code import (L, optional) - -1. Port `src/vscode.rs` (import pipeline, mapping tables, hardening, reports, package compilation, security bounds) and `src/library.rs` (durable library, atomic recoverable saves, last-known-good reload, duplicate-as-editable). Add `json5` + `plist` deps. -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. - -| Old role | New (mechanical) | Intent / notes | -| --- | --- | --- | -| `background` | `background` | | -| `surface_background` | `shell` | sidebars, secondary surfaces | -| `elevated_surface_background` | `raised` | hover cards, popovers, lists | -| `panel_background` | `shell` | | -| `overlay` | `overlay` | scrim/backdrop (domain role is a surface; blend as needed) | -| `title_bar` | `shell` | | -| `window_border` | `border_strong` | | -| `border` | `border` | | -| `border_variant` | `border` (optionally `.opacity(...)`) | | -| `border_focused` / `border_selected` | `accent.primary` | | -| `border_transparent` | `gpui::transparent_black()` | | -| `border_disabled` | `border` low alpha | | -| `ring` | `accent.primary` | focus ring (also via `sync_base`) | -| `text` | `text` | | -| `text_muted` | `text_muted` | | -| `text_placeholder` | `text_faint` | | -| `text_accent` | `accent.primary` | | -| `text_danger` | `danger` | | -| `text_warning` | `warning` | | -| `icon` | `text_muted` | | -| `icon_muted` | `text_faint` | | -| `icon_accent` | `accent.primary` | | -| `element_background` | `accent.primary` (emphasis) / `solid` (neutral) | §8 button redesign | -| `element_foreground` | `accent.on` / `on_solid` | | -| `element_hover` | `mix(accent.primary, background, ~0.12)` | precompute on `ResolvedAccentRoles` if reused | -| `element_active` / `element_selected` | `mix(accent.primary, background, ~0.2/0.28)` | | -| `element_disabled` | `accent.primary.opacity(0.3)` | | -| `secondary_background` | `accent.wash` | tinted chips/selections | -| `secondary_foreground` | contrast-safe accent text (precomputed) | e.g. `accent.primary.ensure_contrast(wash, 4.5)` at resolve time | -| `secondary_hover` / `secondary_active` / `secondary_selected` | `accent.selection` / deeper wash alphas | | -| `secondary_disabled` | `accent.wash.opacity(0.5)` | | -| `danger_background` | `danger_muted` (tint) or `danger.opacity(0.15)` | | -| `danger_foreground` | `danger` (ensure contrast on tint) | | -| `danger_hover/active/selected` | `danger.opacity(...)` steps | | -| `danger_disabled` | `danger.opacity(0.12)` | | -| `warning_*` | same pattern from `warning` / `warning_muted` | | -| `ghost_element_background` | `gpui::transparent_black()` | | -| `ghost_element_background_alt` | `raised` (or `hover`) | | -| `ghost_element_hover` | `hover` | | -| `ghost_element_active` | `active` | | -| `ghost_element_selected` | `active` | | -| `ghost_element_disabled` | `border.opacity(0.4)` | | -| `tab_background` | `shell` | | -| `tab_foreground` | `text_muted` | | -| `tab_hover_background` | `hover` | | -| `tab_active_background` | `background` | active tab merges into the surface | -| `tab_active_foreground` | `text` | | -| `scrollbar_thumb_background` | `border.opacity(0.7)` | scrollbar painting mostly via `gpui-base` | -| `scrollbar_thumb_hover_background` | `border_strong` | | -| `scrollbar_track_background/border` | transparent / `border` | | -| `drop_target_background` | `accent.selection` | | -| `cursor` | `cursor` | | -| `selection` | `accent.selection` | | -| `radius`, `radius_lg`, `shadow`, `font_family`, `font_size`, `notification`, `scrollbar_mode` | unchanged | app chrome on `Theme` | - -Roles with no consumer after migration (`success`, `diff_*`, `terminal`, `syntax`) remain domain-only, available for future features. - -## 8. Component redesign notes (`ui` crate, Phase 3.1) - -- **`Button` variants** (`button.rs`): - - `Primary` → `accent.primary` bg, `accent.on` fg, hover = mix toward background (precomputed on `ResolvedAccentRoles` as `primary_hover`/`primary_active`). - - `Secondary` → `accent.wash` bg, contrast-safe accent fg. - - `Danger` → `danger` bg, `danger.best_on_color()` fg (precompute); tinted variant uses `danger_muted`. - - `Warning` → `warning` bg + best-on fg / `warning_muted` tint. - - `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`. -- **Inputs** (`input/`): bg `input`, border `border`, focus ring `accent.primary`, placeholder `text_faint`. -- **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. - -## 9. `sync_base()` mapping (new) - -| `gpui-base` token | Source | -| --- | --- | -| `appearance` | `theme.appearance` | -| `tokens.background` / `foreground` | `background` / `text` | -| `tokens.surface` / `surface_foreground` | `shell` / `text` | -| `tokens.primary` / `primary_foreground` | `accent.primary` / `accent.on` | -| `tokens.secondary` / `secondary_foreground` | `raised` / `text` | -| `tokens.muted` / `muted_foreground` | `hover` / `text_muted` | -| `tokens.accent` / `accent_foreground` | `accent.wash` / `text` | -| `tokens.destructive` / `destructive_foreground` | `danger` / precomputed on-danger | -| `tokens.border` / `input` | `border` / `input` | -| `tokens.ring` | `accent.primary` | -| `tokens.selection` | `accent.selection` | -| `scrollbar.mode` | `scrollbar_mode` (unchanged) | -| `typography.sans` | `font_family` (unchanged) | - -## 10. Settings migration - -- Old `.settings`: `{ "theme": "themes/.json" | null, "theme_mode": "Light"|"Dark", … }`. -- New: `theme_selection: ThemeSelection` (default `zeron-light`/`zeron-dark`), appearance setting, `accent_selection`, `surface_preference` (all `#[serde(default)]`). -- 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. - -## 11. Dependency changes - -| Crate | Change | -| --- | --- | -| `crates/theme` | `+ thiserror`, `+ sha2`, `+ json5`, `+ plist` (Phase 6); `− schemars`; keep `gpui`, `gpui-base`, `anyhow`, `log`, `serde`, `serde_json` | -| `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. - -## 13. Validation checklist (final) - -- [ ] `cargo build` (desktop), `cargo build -p coop_web`, `cargo test` workspace-wide, `cargo clippy`. -- [ ] Fresh install: zeron light/dark defaults; OS appearance sync; appearance toggle. -- [ ] Theme picker lists builtin families grouped correctly; selection persists per appearance across restarts. -- [ ] Accent preset override recomputes accent-derived UI (buttons, focus ring, selection) live. -- [ ] Old `.settings` loads without error (theme resets, mode preserved); round-trip saves the new format. -- [ ] `gpui-base` projection correct: focus ring, text selection wash, scrollbar styling, overlay backdrop follow the active variant/accent. -- [ ] No references to removed roles remain (`grep` from Phase 3 acceptance). -- [ ] (If Phase 6) import/report/last-known-good flows work; web build unaffected.