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 01fa48da..00000000 --- a/crates/theme/README.md +++ /dev/null @@ -1 +0,0 @@ -CREDITS: [zed/theme](https://github.com/zed-industries/zed/tree/main/crates/theme) diff --git a/docs/ui-theme-bezel-migration.md b/docs/ui-theme-bezel-migration.md deleted file mode 100644 index c2c5e376..00000000 --- a/docs/ui-theme-bezel-migration.md +++ /dev/null @@ -1,574 +0,0 @@ -# UI theme migration to bezel tokens - -Migrate `crates/theme` and `crates/ui` from the current `ThemeColors` / -12-step-scale system to bezel's token set and theme architecture, keeping every -existing UI element (button, dialog, dock, menu, ...) and its public API. - -Upstream: — docs at - (append `.md` to any docs URL; `llms-full.txt` has the -whole corpus). The source of truth for exact constants is bezel's repo: -`crates/theme/src/theme/{mod,palettes,layout,typography,syntax}.rs`. - -> **Status:** Phases 0–1 are done on branch `new-theme` (except the manual -> screenshot pass). The token core lives in `crates/theme/src/{color,palette, -> brand,paint,layout,syntax,appearance}.rs`; the legacy fields remain as -> derived aliases so the whole workspace still compiles. Divergences from the -> original plan are noted inline. - -## Goal - -- One semantic token set (bezel's) with two designed palettes (dark, light). - Light is designed, not inverted — bezel's doc comment on - `crates/theme/src/lib.rs` is the reference for why. -- Theming by `Brand` (tint hue, accent hue, base radius) plus a palette-builder - hook that survives light/dark switches. -- Components read the theme global at paint time and take no color, font, or - size parameters. Radii derive from one base radius by ratio; spacing from - named constants. - -## Non-goals - -- No new components from bezel's catalog. The current element set stays. -- No glass/material/vibrancy port in the first pass. The tokens exist in bezel's - `Theme`, but coop has no backdrop-blur surfaces today; port the fields as - inert defaults or omit them (see decision D6). -- No `bezel` dependency. See D1. - -## Decisions - -- **D1 — Adapt, do not depend (verified 2026-10-09).** bezel 0.2.10 is built - against `bezel-gpui` 0.3.17, published from `crabtalk/zed`. coop is on - `gpui-pre` 0.3.8, a zed snapshot published by longbridge (huacnlee), and - `gpui-base` 0.7.1 (longbridge's gpui-kit) is built against that line with no - bezel-gpui build available. Adding `bezel` would put two gpui forks in the - graph — two type universes, the failure mode bezel's own docs warn about - first — and would strand `gpui-base` (BaseButton, component_traits, - scrollbars). So the theme *architecture* is ported: token names, palette - values, brand rotation, layout constants, and paint helpers are copied - verbatim from bezel's MIT-licensed source (each ported file carries an - attribution header), and gpui paths stay as they are. Revisit only if - `gpui-pre` and `bezel-gpui` ever unify. -- **D2 — Keep component APIs.** `Button`, `Dialog`, `dock`, `menu`, `Sizable`, - `Size`, `ButtonVariant`, ... keep their names and signatures. Variants are - *re-derived* from the new tokens inside `crates/ui`. Callers only break where - they read theme tokens directly (there are ~370 such sites; Phase 3). -- **D3 — Semantic tokens replace scales as the public API.** The Radix-style - 12-step `ColorScale`/`ColorScaleSet` (`crates/theme/src/scale.rs`) stop being - public API. The oklch-derived palette generator replaces them internally. - `ActiveTheme` (`cx.theme()`) stays — it is coop's `Theme::of(cx)`. -- **D4 — JSON themes become overrides.** `assets/themes/*.json` (aurora, 4x - catppuccin, flexoki, forest, ocean) and `ThemeRegistry` survive, but the - schema changes from "full `ThemeColors` dump" to "per-appearance override - deltas + optional `Brand`", applied over the built-in palette by the palette - builder. 8 small files, migrated by hand. -- **D5 — `gpui-base` projection is re-derived, not dropped.** `sync_base` - (`crates/theme/src/lib.rs`) keeps feeding the base global (focus ring, - selection wash, scrollbars, overlay backdrops paint from it). The mapping is - rewritten against the new token names. -- **D6 — Glass/material fields: omitted, not stubbed.** Phase 1 carried no - `vibrancy`/`glass`/`SurfaceSpec` fields at all — coop has no backdrop-blur - surface, and inert fields would only rot. `Brand` carries `tint`, `accent`, - `radius` and `ink` only. A glass port later adds the fields and the - renderer in one move. -- **D7 — App chrome heights stay.** bezel's `TITLEBAR_HEIGHT = 38`, - `HEADER_HEIGHT = 44` are bezel-app values. coop keeps `TITLEBAR_HEIGHT = 36`, - `TABBAR_HEIGHT = 36`, `SIDEBAR_WIDTH = 240` (`crates/theme/src/lib.rs`) — - they are app layout, not tokens. `BASE_RADIUS` and the spacing constants are - adopted. - -## Token mapping - -Old → new. The census command to re-run at any time: - -``` -grep -rhoE "theme\(\)\.[a-z_0-9]+" --include="*.rs" crates desktop web | sort | uniq -c | sort -rn -``` - -Current usage counts (2026-10-09) are in the right column; zero means the token -is only consumed via `sync_base` or not at all. - -### Surfaces - -| Old (`ThemeColors`) | New (`Theme`) | Notes | Uses | -| --- | --- | --- | --- | -| `background` | `bg` | main content plane | 6 | -| `surface_background` | `surface` | shell / sidebar plane | 16 | -| `elevated_surface_background` | `surface_raised` | review each site; inline cards → `surface_card` | 14 | -| `panel_background` | `surface` | | 3 | -| `overlay` | `scrim()` helper | surfaces mount on `surface_overlay` | 1 | -| `title_bar` | `surface` | currently transparent | 0 | -| `title_bar_inactive` | `surface` | | 0 | -| `window_border` | `border` | | 0 | - -### Borders - -| Old | New | Notes | Uses | -| --- | --- | --- | --- | -| `border` | `border` | | 21 | -| `border_variant` | `border_faint` | dividers, nested frames | 11 | -| `border_focused` | `border_strong` | focus *rings* use `ring` | 1 | -| `border_selected` | `border_strong` | | 1 | -| `border_transparent` | drop | `gpui::transparent_black()` inline | 3 | -| `border_disabled` | `border_faint` | | 0 | -| `ring` | `ring` | | 0 | - -### Text and icons - -| Old | New | Notes | Uses | -| --- | --- | --- | --- | -| `text` | `text` | | 16 | -| `text_muted` | `text_muted` | light value darkened to `neutral(0.45)` (~5.3:1) — AppKit's secondary label was too faint on the chrome grey | 53 | -| `text_placeholder` | `text_faint` | | 25 | -| `text_accent` | `accent` | | 1 | -| `text_danger` | `danger` | soft/inline error copy → `danger_muted` | 10 | -| `text_warning` | `warning` | soft copy → `warning_muted` | 2 | -| `icon` | `text_muted` | bezel has no icon tokens; tone is a text token | 2 | -| `icon_muted` | `text_faint` | | 10 | -| `icon_accent` | `accent` | | 6 | - -### Plates and states - -| Old | New | Notes | Uses | -| --- | --- | --- | --- | -| `element_foreground` | `on_solid` | label on the primary plate | 2 | -| `element_background` | `solid` | bezel's primary button plate is the max-contrast solid; use `accent_strong` + `on_accent` only where a brand plate is wanted | 4 | -| `element_hover` | `element_hover` | | 2 | -| `element_active` | `element_active` | | 5 | -| `element_selected` | `element_active` | | 1 | -| `element_disabled` | `wash()` at disabled alpha | helper, not token | 2 | -| `secondary_foreground` | `text` | tinted secondary buttons use `accent` | 8 | -| `secondary_background` | `surface_raised` | | 5 | -| `secondary_hover` | `element_hover` | | 1 | -| `secondary_active` | `element_active` | | 2 | -| `secondary_selected` | `element_active` | | 1 | -| `secondary_disabled` | `wash()` at disabled alpha | | 1 | -| `danger_foreground` | `on_accent` | over the `danger_strong` plate | 5 | -| `danger_background` | `danger_strong` | destructive plate; tinted chips → `danger_muted` | 4 | -| `danger_hover` / `danger_active` / `danger_selected` | plate-hover helper | one helper that brightens (dark) / darkens (light) a plate | 1/1/1 | -| `danger_disabled` | `wash()` at disabled alpha | | 1 | -| `warning_foreground` | `warning` | `warning_muted` is a translucent amber wash; the label is the amber token itself | 5 | -| `warning_background` | `warning_muted` | translucent amber wash (`warning` at 10% light / 16% dark) | 5 | -| `warning_hover` / `warning_active` / `warning_selected` | `element_hover` / `element_active` | on the chip | 1/1/1 | -| `warning_disabled` | `wash()` at disabled alpha | | 1 | -| `ghost_element_background` | transparent | drop | 6 | -| `ghost_element_background_alt` | `surface_raised` | or `band` for strips | 3 | -| `ghost_element_hover` | `element_hover` | | 7 | -| `ghost_element_active` / `ghost_element_selected` | `element_active` | | 7/1 | -| `ghost_element_disabled` | `wash()` at disabled alpha | | 1 | - -### Tabs, input, misc - -| Old | New | Notes | Uses | -| --- | --- | --- | --- | -| `tab_background` | `surface` | | 2 | -| `tab_foreground` | `text_muted` | | 1 | -| `tab_hover_background` | `element_hover` | | 2 | -| `tab_active_background` | `element_active` | one rung past `element_hover` (darker in light, brighter in dark); hairline separates from the plane | 1 | -| `tab_active_foreground` | `text` | | 2 | -| `scrollbar_*` (5 tokens) | gpui-base scrollbar tokens | via `sync_base` only | 0 | -| `drop_target_background` | `drop_target` | | 1 | -| `cursor` | `cursor` | | 1 | -| `selection` | `selection` | | 0 | -| `radius` | `Theme::button_radius()` | 1x; controls inside controls → `control_radius()` (0.75x). Review each of the 43 sites | 43 | -| `radius_lg` | `panel_radius()` (1.25x) or `surface_radius()` (1.5x) | cards → panel, dialogs/popovers → surface | 8 | -| `shadow` | `surface_shadows()` helper | keep the on/off setting | 10 | -| `font_size` | `base_text_size()` | stays 15px | 1 | -| `font_family` | `font_sans` | | 1 | -| `notification` | stays | `NotificationSettings` remains a coop extension field | 2 | - -### Tokens coop gains - -`text_dim` (one notch below muted), `success` / `success_muted` / `busy` -(message delivery and presence states), `accent` / `accent_strong` / -`on_accent`, `input_bg`, `caret`, `band`, `surface_card` / `surface_dialog` / -`surface_overlay` (elevation ladder), `surface_raised_hover`, `drop_line`, -`code_text` / `code_wash` / `syntax: SyntaxPalette` / `diff_add` / `diff_del` / -`diff_hunk_bg` (for `markdown.rs`), and the paint helpers `ink(alpha)` / -`hairline(alpha)` / `wash(alpha)` / `scrim()`. - -### Tokens coop drops - -The `ColorScale`/`ColorScaleSet` public API, `ThemeColors` as a public struct, -`element_*`/`secondary_*`/`ghost_element_*`/`tab_*`/`warning_*`/`danger_*` -families, `icon*` tokens, `title_bar*`, `window_border`, `scrollbar_*` on the -coop theme. - -## Phases - -Each phase is one PR (or a small series), compiles, and ships with the -validation listed. Follow the PR hygiene in `AGENTS.md`. - -### Phase 0 — Baseline - -- [x] Record the token census (command above) as the migration checklist. -- [x] Branch: work happens on `new-theme`. -- [ ] Capture light/dark screenshots of the main surfaces: chat panel, - message hover/actions, relays & trace dialogs, settings dialog, group - (nip29) views, workspace tabs/dock, notifications. *(manual — run the - app before and after Phase 2/3)* - -### Phase 1 — Port the theme core into `crates/theme` (done) - -All steps keep the workspace compiling: the legacy token fields remain as -values derived from the tokens (`ThemeColors::sync_legacy`), so the ~370 call -sites build unchanged. - -- [x] **Palette generator.** `palette.rs`: bezel's two designed palettes, - constants copied verbatim from bezel's `palettes.rs` (oklch-derived - neutrals via `color.rs`). Light is designed, not inverted. -- [x] **Token set.** `colors.rs`: the bezel token fields are flat on - `ThemeColors` (bg, surface ladder, hairlines, text ladder, solid, - accent trio, semantic colors, components, code & diff, syntax palette). - Legacy names resolve through `sync_legacy`. Deserialization refines over - the built-in dark palette (`#[serde(default)]`), so partial JSON still - loads. -- [x] **Appearance plumbing.** `appearance.rs`: `AppearanceMode::{System, - Light, Dark}` (serde), `init`, `set_mode`, `apply`, `observe_window`. - `Theme::sync_system_appearance` routes through it. *Divergence: bezel's - macOS `NSAppearance` override syncing (so pinned themes restyle the - traffic lights) is not ported — needs an `objc` dependency; follow-up.* -- [x] **Brand.** `brand.rs`: `Tint`, `BASE_COLORS` (Tailwind's five neutral - families), `TextInk`/`Ink` (AppKit + Accessible), `Brand { tint, accent, - radius, ink }` with the grey-rotation rule (grey tokens take the tint, - semantic tokens keep their hue, translucent ink is skipped). - `AppExt::set_brand` / `set_palette` install globals; the palette builder - runs first on every appearance install. A default brand is a strict - no-op so JSON themes are untouched until an app brands itself. -- [x] **Layout constants.** `layout.rs`: `SPACE = 8`, `CONTENT_MARGIN = 20`, - `EDGE = 12`, `BASE_RADIUS = 8`, `BUTTON_HEIGHT = 24`, - `CONTROL_HEIGHT_SMALL = 20`, ratio functions (control 0.75x, button 1x, - panel 1.25x, surface 1.5x, bubble 2x), `inset_radius`. `Theme::radius` - is now 8 and `radius_lg` 12 (was 6/10) — intentional visual change. - coop's chrome heights stay (D7). -- [x] **Typography (partial).** `Theme` gains `font_body` (defaults to - `font_family`) and `font_mono` (per-backend system mono). *Divergence: - bezel's `TextStyle`/`Metrics`/`Typeset` ladder and `ControlSize`/ - `Sizing` are deferred to Phase 2, where the components that would use - them get reskinned.* -- [x] **Paint helpers.** `paint.rs`: process-wide appearance mirror, `ink`, - `hairline`, `wash`, `scrim`, `band`, `INK_FILL_SCALE`/ - `INK_HAIRLINE_SCALE`, `surface_shadows`, plate hover/active helpers. -- [x] **Legacy JSON bridge.** `ThemeFamily::from_file` and the registry's - loader call `ThemeColors::rederive_tokens`, which best-effort maps an - old-schema theme's fields back onto the tokens (fields with no legacy - source — `success`, `busy`, syntax, diff — keep the built-in palette's - value for that appearance). Phase 4 replaces the schema; this bridge is - what keeps the 8 shipped themes usable in the meantime. -- [x] **`sync_base` rewrite.** Projects the new tokens onto `gpui_base::Theme` - (bg→background, text→foreground, solid→primary, surface_raised→secondary, - surface_card→muted, element_hover→accent, danger_strong→destructive, - on_solid/on_accent→*_foreground, border→border/input, ring→ring, - selection→selection). -- [x] **Contrast tests.** `tests/contrast.rs`: text ≥ 13.9:1 on bg and - surface in both appearances (measured 14.4/15.1), muted ≥ 3.9:1, - faint ≥ 1.8:1 (AppKit ink lands below WCAG AA by design), light-vs-dark - parity for primary text within 0.7, default brand is a no-op, brand - rotation moves greys but not semantic hues, legacy aliases track - tokens, and a legacy-JSON round trip. *Note: bezel's docs claim ~0.5 - parity for every text role; the shipped AppKit ink only delivers that - for primary text, so the test asserts floors for muted/faint instead.* -- [x] `cargo test -p theme` (11 tests) and `cargo check --workspace` green; - clippy clean. -- [ ] Pixel review of the running app against Phase 0 screenshots (needs the - manual screenshot pass first). The palette swap is intentionally visible: - neutral `#060606`/`#0d0d0d` surfaces replace the grey scale, hover/active - washes are neutral ink, focus rings are hairlines, cursor/selection match - macOS defaults. - -### Phase 2 — Reskin `crates/ui` - -Keep public APIs (D2). Per file, replace old token reads with new tokens, -radii with ratio functions, ad-hoc spacing with `SPACE` steps where it is a -real deviation, and state washes with the paint helpers. - -- [x] `button.rs` — variant table: Primary → `solid`/`on_solid` + - `surface_raised_hover`-style plate hover; Secondary → - `surface_raised` + `border` + `text`; Danger → `danger_strong` + - `on_accent`; Warning → `warning_muted` chip treatment; Ghost → - transparent + `element_hover`/`element_active`; Custom stays (sources - defaults from new tokens). Size → bezel `ControlSize` ladder values via - the existing `Size` enum. Radius: `button_radius()`, `bubble_radius()` - when `rounded`. *(Size ladder heights kept — bezel's 24px control - heights are a consumer-visible layout change, deferred.)* -- [x] `input/` — `input_bg`, `border_faint` resting / `border_strong` focus, - `ring`, `caret`, `selection`, `text_faint` placeholder. *(coop's Input - paints no frame border/ring of its own — focus mechanics untouched; - `InputEditorStyle::border` now `border_faint`, placeholder reaches - gpui-base's `muted_foreground`, now `text_faint`.)* -- [x] `dialog.rs` — `surface_dialog` over `scrim(SCRIM_ALPHA_DARK)`, - `surface_radius()`, `surface_shadows()` (entrance animation scales the - helper's shadow alphas). -- [x] `popover.rs`, `menu/` — `surface_overlay`, hairline border, - `popover_surface` stub (D6: plain wash for now). *(The floating plate - is `StyledExt::popover_style` in styled.rs; popover.rs itself paints no - theme values. Menu item corners are concentric: - `inset_radius(surface_radius(), 4)`.)* -- [x] `tab.rs` — mapping table row: `bg` active plane, `element_hover` - hover, `text_muted`/`text` labels, `border_faint` separators. *(No - separator was painted before; none added — structure frozen. Segmented - track is `surface`.)* -- [x] `nav.rs`, `nav_item.rs` — `element_hover`/`element_active` states, - `accent` selection markers. *(Resting labels are now `text_muted` — - intentional visible change; radius 12→8.)* -- [x] `title_bar.rs` — the crate never read `title_bar`/`title_bar_inactive` - (consumers paint the strip); close-button danger hover reskinned to - `danger_strong`/`on_accent`/`plate_active`. Drag handlers and heights - untouched. -- [x] `tooltip.rs` — `surface_overlay` plate, `text` label, - `control_radius()`. -- [x] `switch.rs` — on: `accent_strong` track + `on_solid` knob; off: - `input_bg` track + `border`. -- [x] `indicator.rs`, `kbd.rs` — indicator turned out to be the loading - spinner and paints no theme values (no status dots exist in `crates/ui` - — the `success`/`busy`/`danger` mapping lands at consumer call sites in - Phase 3). kbd: `surface_raised` + `border_faint` + `text_muted`. -- [x] `avatar.rs` — no ring/border is painted today; dropped the - `ghost_element_background` underlay. Blob behavior kept. -- [x] `message.rs` — `text_dim` on the author/timestamp row, `text_faint` - welcome body, `element_hover` row wash. *(No bubbles, delivery states, - composer strips or report banners live in this file — those are - consumer-side, Phase 3.)* -- [x] `markdown.rs` — `code_text`/`code_wash` (added `with_inline_code`), - heading hierarchy (`with_heading`: h1–h3 `text`, h4–h6 `text_muted`), - links `accent`. *(Fenced-code highlighting comes from gpui-base's theme - via `sync_base`; `SyntaxPalette` stays unconsumed until a coop-side - highlighter exists.)* -- [x] `notification.rs` — `surface_overlay` plate + `surface_radius()` + - `surface_shadows()`; error is a full `danger_strong` plate with - `on_accent`, success/warning icons `success`/`warning`. -- [x] `group_box.rs` — `surface` plate, `panel_radius()`. *(No border or - header strip is painted today; adding them is a design change, deferred - to Phase 3/4 review.)* -- [x] `dock/`, `resizable/` — `drop_target`, `drop_line`, `border_faint`. - *(Tab-insertion line and drop-region outline both use `drop_line` now — - slightly more prominent than the old muted outline, intentional. Drag - preview/empty-space drag-over also reskinned.)* -- [x] `scroll/` — verified: zero theme reads; scrollbar paint comes from - gpui-base via `sync_base`, `ScrollbarMode` still lands. -- [x] `root.rs` — `bg` plane, `surface_shadows()` on the window frame (now - gated on `cx.theme().shadow`); decoration consts untouched. -- [x] `icon.rs` — default tone `text_muted` (was `icon`); `IconName` set and - APIs unchanged. -- [x] `styled.rs` — `divider()` → `border_faint`; `popover_style()` → - `surface_overlay` + `px(surface_radius())` + `surface_shadows()` gated - on `cx.theme().shadow` (was always-on `shadow_md`). Keep - `h_flex`/`v_flex` names (bezel's `stack::row`/`column` equivalents). -- [x] `animation.rs` — left as-is this pass; bezel's named-motion catalog is - a follow-up. - -Outcome: `theme::plate_hover`/`plate_active` were made public (wrappers over -the crate-internal `*_for` helpers) so `crates/ui` can reskin opaque plates. -Zero legacy-token reads remain in `crates/ui` (the Phase 3 grep already -passes inside this crate); `cargo check -p ui`, `cargo check --workspace`, -`cargo test -p theme` (11 tests) and `cargo clippy -p ui -p theme` are green. -Public APIs, element structure, sizes and heights unchanged (D2, D7). - -Light-mode review fixes (after running the app): - -- **Warning chip is a wash, not a plate.** Bezel's `warning_muted` is a - strong amber (800 light / 200 dark) meant to carry white/black ink; on a - button it read as muddy brown. Both appearances now derive - `warning_muted` as a translucent wash of the `warning` token (10% light, - 16% dark) and pair it with `warning`-toned labels; button hover/active - deepen the plate with the plate helpers, and `sync_legacy` maps - `warning_foreground` → `warning`. A contrast test measures the label - against the wash flattened onto the content plane. -- **Inputs lost their grey.** Light `input_bg` was pure white; it is now - `neutral(0.950)`, one rung below the chrome grey like the old scale's - step-3 input plate. Dark stays bezel's white-at-3%. -- **Dock planes move to `bg`.** The dock tab bar, title strip and panel - container read `bg` (white light / `#060606` dark — bezel's main panel), - matching the old near-white `panel_background`. Sidebar keeps `surface` - (the chrome grey). -- **Active tab is a wash, not white.** The dock tab's selected plane is - `element_active` — one rung past the `element_hover` hover (darker in - light, brighter in dark). The segmented tab's active segment stays a - white raised plate with a shadow. `sync_legacy.tab_active_background` - follows. -- **Sidebar text contrast.** Light `text_muted` moves from AppKit's - secondary label (~4:1, ~3.7:1 on the grey sidebar) to `neutral(0.45)` - (~5.3:1 on white), which also narrows the dark/light muted gap. Faint - stays AppKit-faint by design (placeholders). -- `Brand::branded` now skips `apply` for the default brand so the test's - no-op assertion holds now that a shipped token (`text_muted`) diverges - from the AppKit ink `apply` would re-assert. -- **Danger plates carry white ink in both appearances** (bezel's - destructive pairing — their `buttons.rs` hardcodes `gpui::white()` over - `danger_strong`): Danger button, error notification plate, title-bar - close hover, and `sync_base.destructive_foreground`. Visually a dark-mode - change only; light `on_accent` was already near-white. - -Validation remaining: run desktop, walk every component in light/dark -against the Phase 0 screenshots. - -### Phase 3 — Migrate consumers - -Mechanical per the mapping table; ~370 sites across 8 crates. Use the -deprecated accessors as the worklist — the goal is zero deprecated hits. - -Semantic states, per bezel's own widgets (`crates/ui/src/widgets/{status, -content,buttons}.rs`) — the recipes consumer banners, pills and state marks -should follow: - -- **Strips (error/warning).** Bezel ships exactly two — "two tones, not a - level enum"; there is no info strip. Recipe: background = the semantic - token at 6% opacity, hairline ring = the same token at 20%, label and - leading triangle icon = the family's ink step at 90%, all at - `surface_radius()`. In coop tokens: error → bg `danger` at 6%, ring - `danger` at 20%, ink `danger_muted` (we kept bezel's ink step for danger); - warning → bg `warning_muted` (our wash token), ring `warning` at 20%, ink - `warning`. -- **Success pill.** Bezel's `badge_active`: background = `success` at 12%, - ink = `success_muted` at 90%, `rounded_full`, no border. -- **State dots.** No enum — the caller passes the tone - (`success`/`busy`/`warning`/`danger`, `text_faint` for "nothing yet") to a - 6px bead. -- **Destructive controls.** Bezel's plate is `danger_strong` with **white - ink in both appearances** (`gpui::white()` in their `buttons.rs`), hover - dims the whole control to 90% opacity, and role is independent of emphasis - (a quiet destructive control is a ghost with `danger_strong`-toned text). - coop now carries the white-ink rule in button, notification error plate, - title-bar close hover, and `sync_base.destructive_foreground`. -- **Info.** No semantic tone — neutral copy on `text_muted`, or the quiet - hairline badge. -- Known divergence (kept deliberately): bezel keeps `warning_muted` as the - family's ink token and bakes wash alphas at call sites; coop bakes the - warning wash into the `warning_muted` token (10% light / 16% dark) because - the legacy `warning_background` alias and the button plate need a token, - and pairs it with `warning`-toned ink. The danger family keeps bezel's - exact semantics (`danger` wash source, `danger_muted` ink, `danger_strong` - plate). - -- [x] `crates/chat_ui` — all 20 legacy tokens migrated in `lib.rs` (~46 - sites). Radius rulings: message media images → `bubble_radius()`; - reaction/file chips and composer tiles → `control_radius()`; trace - callout → `panel_radius()`; warning/danger report strips and the - floating hover toolbar → `surface_radius()`. `elevated_surface` - split: relay chips / reply-row hover → `surface_raised`, success - report box → `surface_card`. Warning sites got the bezel strip - treatment (`warning_muted` wash + `warning` ink); failed-report rows - got the error-strip recipe (`danger` @6% + `danger_muted` @90%). -- [x] `crates/workspace` — 58 sites across 18 files. Radius: warning - strips → `surface_radius()`; input plates and control rows → - `button_radius()`; request-count chip → `control_radius()`; browse - cards and empty states → `panel_radius()`. `text_danger` → `danger` - everywhere (each site was its view's only error signal). The "new - requests" bead in the sidebar (old `cursor` token, an accent blue) - → `accent`, not `caret` — it is an attention marker, not a text - cursor. Settings dialog's `Theme::change` calls left for Phase 4. -- [x] `crates/nip29_ui`, `crates/nip4e` — 27 sites. Activity-row hover → - `element_hover` (it interleaves with `MessageRow` in one list); - message media → `bubble_radius()`; chips/pills → `control_radius()`; - nip4e plates → `surface_card`. Flag for the design pass: the pin - chip now paints `solid` under a `text_muted` label — that pairing - predates the migration (old `element_background` + `text_muted`); - if it reads loud, its label should become `on_solid`. -- [x] `crates/settings`, `desktop`, `web` — verified zero theme-token - reads (their appearance wiring is Phase 4 scope). -- [x] Replace `radius`/`radius_lg` with the ratio function the element is - — done per crate by the agents above (not sed); rulings recorded - per bullet. -- [x] `cargo check --workspace` clean; the Phase 3 grep gate (extended - with `surface_background`, `background`, `cursor`, `drop_target_background`) - returns nothing across crates and `desktop`/`web`. - -Follow-ups noted: chat_ui paints delivery/status marks with hardcoded -`gpui::green()`/`red()` — candidates for the `success`/`busy`/`danger` -status-dot recipe in a later polish pass; warning strips carry no 20%-alpha -ring (no border existed; adding one is a structure change). - -### Phase 4 — Settings, appearance, and the removal of the multi-theme system (done) - -The original plan (appearance_mode with back-compat, registry re-schema, -brand-izing the 8 shipped JSONs) was superseded by a decision: **coop no -longer ships multiple themes**. The bezel token palettes (dark and light, -optionally rotated by a `Brand`) are the only theming surface. Done: - -- [x] `crates/settings`: persisted `theme_mode: ThemeMode` → - `appearance_mode: AppearanceMode` with - `#[serde(default, alias = "theme_mode")]` — an old config's pinned - Light/Dark survives; absence falls back to `System`. Round-trip tests - cover both. `set_theme`/`reset_theme` deleted; `apply_theme` is now - `cx.set_appearance_mode(mode)`. -- [x] `ThemeRegistry`, `assets/themes/*.json` (8 files), and the - `themes/**/*` asset embed are deleted. `theme::init` is - `appearance::init` + scrollbar sync. -- [x] `ThemeFamily`, `from_file`/`from_assets`, the theme picker dialog - (`theme_selector` + `Command::ToggleTheme` + its header/greeter menu - entries), and the settings dialog's "Reset theme" row are deleted. - The appearance picker is now System/Light/Dark via - `AppearanceMode::ALL` and `AppExt::set_appearance_mode`. -- [x] The old-schema JSON bridge (`rederive_tokens`, `sync_legacy`, - `legacy_default`) is gone with the schema. - -### Phase 5 — Removal and docs (done) - -- [x] Deleted `ThemeColors` legacy fields (~60), `ColorScale`/`ColorScaleSet` - (`scale.rs`), `ThemeColors::light/dark` wrappers, `Theme::apply_theme`, - and the `Theme` fields `theme: Rc`, `radius`, `radius_lg` - (every site reads the ratio functions now). `ThemeColors` stays public: - it *is* the token set, in the bezel style. -- [x] `crates/ui/src/scroll/scrollable.rs` no longer paints scrollbar colors - from coop tokens — gpui-base's `Scrollbar` paints from its own global - theme (kept in step by `sync_base`'s scrollbar mode), with base's - defaults for thumb/track. -- [x] `ui::init` boot order verified: `theme::init` (appearance::init) - runs before the first window in desktop and web; workspace still - observes window appearance for live System flips. Desktop/web - comments updated. -- [x] Final census: only new token names remain; workspace compiles; tests - green (theme 5, settings 2 incl. serde compat, 52 chat_ui). - -Follow-ups noted: chat_ui's hardcoded `gpui::green()`/`red()` status marks -are candidates for the `success`/`busy`/`danger` status-dot recipe; the -nip29 pin chip pairs `solid` with a `text_muted` label (predates the -migration) — flip its label to `on_solid` if it reads loud. - -### Phase 5 — Removal and docs - -*(Executed together with Phase 4 — see above.)* - -- [x] Delete `ThemeColors`, the deprecated aliases, `ColorScale`/ - `ColorScaleSet` public exports (`scale.rs` either goes private as the - palette generator's internal or is deleted), `ThemeMode`. -- [x] `crates/ui/src/lib.rs`: `theme::init` already runs `appearance::init`; - verify `ui::init`'s callers boot the theme before the first window, and - that every opened window observes appearance changes - (`theme::observe_window` or workspace's sync-on-activation hook). -- [x] Update `README`/docs mentions of theme customization; document - `Brand` + override JSON as the two supported ways to theme coop. -- [x] Final census run: only new token names remain. - -## Risks - -- **Fork mismatch (D1).** If `gpui-pre` and `bezel-gpui` ever unify, a direct - `bezel` dependency becomes possible and Phase 1's ported code could be - swapped for the real crate. Do not half-adopt (bezel + own gpui) — that is - the two-gpuis trap. -- **Light regressions.** coop's light palettes today are scale-derived; the - new light palette is designed. Expect intentional differences in surface - order and accent tones; judge against the Phase 0 screenshots, and record - intentional deltas in the PR description. -- **gpui-base drift.** Anything base paints (focus ring, text selection, - scrollbars, overlay backdrops) bypasses coop tokens. `sync_base` is the only - seam — keep it total, and keep it on the appearance-change path. -- **Init order.** `appearance::init` must run before the first window opens or - the first frame paints the wrong palette; `observe_window` must be attached - per window for live light/dark flips. -- **Settings serde.** Old persisted `theme_mode` values must not break load; - covered by Phase 4 compat shim and a round-trip test. -- **No component gallery.** coop has no bezel-gallery equivalent; the Phase 0 - screenshots are the review surface. If Phase 2 review gets painful, a - throwaway debug window that renders one of each component is cheaper than - regressions. - -## References - -- bezel theme docs: , - , , - -- bezel source (copy constants from here): - `crates/theme/src/theme/{mod,palettes,layout,typography,syntax}.rs`, - `crates/theme/src/{lib,paint,brand}.rs` -- Current coop theme: `crates/theme/src/{lib,colors,scale,theme,registry}.rs` -- Current coop components: `crates/ui/src/`