325 lines
27 KiB
Markdown
325 lines
27 KiB
Markdown
# 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<path, Rc<ThemeFamily>>` |
|
||
| `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<String>` (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<ThemeVariant>`, 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<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.
|
||
|
||
---
|
||
|
||
## 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<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;
|
||
- `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<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.
|
||
|
||
### 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/<name>.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.
|