diff --git a/DESIGN.md b/DESIGN.md index 95978ac569..528876d1bf 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -13,7 +13,7 @@ colors: brand-cyan: "#78bce8" ombre-start: "#1535B2" ombre-end: "#6AA6DC" - # the sheet — crates/tui/src/palette/tokens.rs, exported to web/app/tokens.css (generated, never hand-edit) + # the sheet — crates/palette/src/tokens.rs, exported to web/app/tokens.css (generated, never hand-edit) paper: "#f6f2e8" # WHALE_TEXT_BODY, Whale Ivory — the page above the waterline paper-deep: "#e8eef8" # LIGHT_ELEVATED — the shallows; cards and code-adjacent plates paper-card: "#fffdf8" # LIGHT_PANEL — a raised sheet on the paper @@ -135,8 +135,11 @@ Hard rules, not taste notes. The strata are geometry drawn from tokens — never a hex of their own. 2. **No gradients as decoration elsewhere.** The ombre `#1535B2 → #6AA6DC` lives in the mark, the wordmark, and the water. The ocean column's field is - the TUI's own chrome → bg descent; no spotlight glows, gradient text, or - gradient rules on paper. + the site's own chrome → bg descent — the navy water column is the TUI's + selectable `underwater` theme, not the ground the terminal opens on. That + ground is Shoreline, the warm charcoal and single blue shared with the GPUI + client (`docs/design/TUI_REDESIGN.md`). No spotlight glows, gradient text, + or gradient rules on paper. 3. **One shadow.** The terminal plate at the waterline casts one soft, offset, blue shadow. Nothing else on the site has a drop shadow. 4. **No generic SaaS scaffolding.** No icon-card grids, logo walls, @@ -157,7 +160,7 @@ Hard rules, not taste notes. ## Colors -One palette, owned by `crates/tui/src/palette/tokens.rs` and exported to +One palette, owned by `crates/palette/src/tokens.rs` and exported to `web/app/tokens.css` by `scripts/export-design-tokens.py` — both the `WHALE_*` dark tokens (`--whale-*`) and the Blue Stage light preset's `LIGHT_*` tokens (`--light-*`). `web/app/globals.css` maps them to the site's diff --git a/PRODUCT.md b/PRODUCT.md index 689de6c5b9..36f74456d0 100644 --- a/PRODUCT.md +++ b/PRODUCT.md @@ -78,11 +78,14 @@ no hosted runtime to sell. - Voice: quiet, dense, factual. Terminal vocabulary, no marketing superlatives, no fabricated transcripts or reasoning traces. - "It doesn't need to look special — it needs to look like Codewhale." -- Assets: the founder's whale mark traced to `brand/mark.svg` (with - `brand/mark-navy.svg`, `brand/mark-gradient.svg`) and the founder's rounded - monoline wordmark traced from `brand/wordmark0901.png` (`brand/wordmark.svg` - navy #142352, `brand/wordmark-inverted.svg` white; regenerated with the site - icons by `scripts/brand/trace-brand.py`). Web copies live in +- Assets: the canonical vector family lives in the CWC repo at + `codewhale-apps/packages/brand/svg/` (mark, mark-gradient, mark-mono, + mark-reversed, wordmark, wordmark-inverted); `brand/` and + `web/public/brand/` carry byte-identical copies — sync from there, never + re-trace. The founder's brand sheet `brand/codewhalemarkfinal.png` remains + the source `scripts/brand/braille-mark.py` derives TUI launch art from. The + earlier local trace and `scripts/brand/trace-brand.py` were retired + 2026-09-15 in favor of the canonical family. Web copies live in `web/public/brand/`. - Palette, type, shell direction, and the anti-slop rules are recorded in `DESIGN.md`; the colour tokens are owned by `crates/tui/src/palette/tokens.rs` diff --git a/brand/mark-gradient.svg b/brand/mark-gradient.svg index 412eb6c121..412e4c14d9 100644 --- a/brand/mark-gradient.svg +++ b/brand/mark-gradient.svg @@ -1 +1,9 @@ - + + + + + + + + + diff --git a/brand/mark-mono.svg b/brand/mark-mono.svg new file mode 100644 index 0000000000..f67e8d277b --- /dev/null +++ b/brand/mark-mono.svg @@ -0,0 +1,3 @@ + + + diff --git a/brand/mark-navy.svg b/brand/mark-navy.svg deleted file mode 100644 index 4272f677ae..0000000000 --- a/brand/mark-navy.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/brand/mark-reversed.svg b/brand/mark-reversed.svg new file mode 100644 index 0000000000..90ceae43ce --- /dev/null +++ b/brand/mark-reversed.svg @@ -0,0 +1,3 @@ + + + diff --git a/brand/mark.svg b/brand/mark.svg index 75b6a4d86d..71d3b534c6 100644 --- a/brand/mark.svg +++ b/brand/mark.svg @@ -1 +1,3 @@ - + + + diff --git a/brand/wordmark-inverted.svg b/brand/wordmark-inverted.svg index e537014177..b0bb87e19f 100644 --- a/brand/wordmark-inverted.svg +++ b/brand/wordmark-inverted.svg @@ -1 +1,3 @@ - + + + diff --git a/brand/wordmark.svg b/brand/wordmark.svg index 81133ab7e2..03d61e8957 100644 --- a/brand/wordmark.svg +++ b/brand/wordmark.svg @@ -1 +1,3 @@ - + + + diff --git a/brand/wordmark0901.png b/brand/wordmark0901.png deleted file mode 100644 index 23f403178f..0000000000 Binary files a/brand/wordmark0901.png and /dev/null differ diff --git a/brand/wordmarkinverted.png b/brand/wordmarkinverted.png deleted file mode 100644 index e66c3fb32a..0000000000 Binary files a/brand/wordmarkinverted.png and /dev/null differ diff --git a/crates/config/src/settings_schema.rs b/crates/config/src/settings_schema.rs index f854d00de5..d5ce80123b 100644 --- a/crates/config/src/settings_schema.rs +++ b/crates/config/src/settings_schema.rs @@ -404,7 +404,7 @@ pub const SETTINGS_SCHEMA: &[SettingDef] = &[ def( "theme", SettingKind::String, - "underwater", + "shoreline", ui( TAB_APPEARANCE, "display", diff --git a/crates/palette/src/adapt.rs b/crates/palette/src/adapt.rs index 8207a275c5..fda68220c2 100644 --- a/crates/palette/src/adapt.rs +++ b/crates/palette/src/adapt.rs @@ -199,13 +199,18 @@ const fn theme_diff_deleted_bg(ui: &UiTheme) -> Color { /// Returns `true` if the preset participates in the cell-level remap. The /// default Whale and System themes pass through unchanged so this whole -/// stage compiles down to a single load+compare on the hot path. +/// stage compiles down to a single load+compare on the hot path. Shoreline is +/// listed because it is a full re-ink — warm charcoal instead of the navy the +/// direct terminal constants were tuned for — so every one of those call +/// sites has to land on the preset's slots. #[inline] #[must_use] pub const fn theme_remap_active(theme: ThemeId) -> bool { matches!( theme, ThemeId::Terminal + | ThemeId::Shoreline + | ThemeId::ShorelineLight | ThemeId::CatppuccinMocha | ThemeId::TokyoNight | ThemeId::Dracula diff --git a/crates/palette/src/themes.rs b/crates/palette/src/themes.rs index 6dfaa0ecf3..6e6bc3c104 100644 --- a/crates/palette/src/themes.rs +++ b/crates/palette/src/themes.rs @@ -336,6 +336,443 @@ pub const LIGHT_UI_THEME: UiTheme = UiTheme { } .with_terminal_native_shell(); +/// Shoreline — the default shell from 0.9.14, and the terminal's half of the +/// product-client palette. +/// +/// The GPUI desktop window and this terminal now cite the same slots, so the +/// two clients read as one product: a warm charcoal ground, one raised plate +/// for cards and the composer, one restrained blue for action and selection, +/// and the whale's ivory ink. It does **not** call +/// [`UiTheme::with_terminal_native_shell`] — the field is painted, the way the +/// desktop window paints it — and `adapt::theme_remap_active` remaps every +/// direct terminal constant onto these slots, so nothing navy-tuned leaks +/// through. +/// +/// The ocean is not gone, it is a choice: `underwater` keeps the painted +/// water column, the ambient life and the ombre. +/// +/// Every pair `contrast::theme_contrast_violations` audits clears its floor. +pub const SHORELINE_UI_THEME: UiTheme = UiTheme { + name: "shoreline", + mode: PaletteMode::Dark, + surface_bg: Color::Rgb( + SHORELINE_SURFACE_RGB.0, + SHORELINE_SURFACE_RGB.1, + SHORELINE_SURFACE_RGB.2, + ), + panel_bg: Color::Rgb( + SHORELINE_PANEL_RGB.0, + SHORELINE_PANEL_RGB.1, + SHORELINE_PANEL_RGB.2, + ), + elevated_bg: Color::Rgb( + SHORELINE_ELEVATED_RGB.0, + SHORELINE_ELEVATED_RGB.1, + SHORELINE_ELEVATED_RGB.2, + ), + composer_bg: Color::Rgb( + SHORELINE_COMPOSER_RGB.0, + SHORELINE_COMPOSER_RGB.1, + SHORELINE_COMPOSER_RGB.2, + ), + selection_bg: Color::Rgb( + SHORELINE_SELECTION_RGB.0, + SHORELINE_SELECTION_RGB.1, + SHORELINE_SELECTION_RGB.2, + ), + header_bg: Color::Rgb( + SHORELINE_CHROME_RGB.0, + SHORELINE_CHROME_RGB.1, + SHORELINE_CHROME_RGB.2, + ), + footer_bg: Color::Rgb( + SHORELINE_CHROME_RGB.0, + SHORELINE_CHROME_RGB.1, + SHORELINE_CHROME_RGB.2, + ), + text_dim: Color::Rgb( + SHORELINE_TEXT_DIM_RGB.0, + SHORELINE_TEXT_DIM_RGB.1, + SHORELINE_TEXT_DIM_RGB.2, + ), + text_hint: Color::Rgb( + SHORELINE_TEXT_HINT_RGB.0, + SHORELINE_TEXT_HINT_RGB.1, + SHORELINE_TEXT_HINT_RGB.2, + ), + text_muted: Color::Rgb( + SHORELINE_TEXT_MUTED_RGB.0, + SHORELINE_TEXT_MUTED_RGB.1, + SHORELINE_TEXT_MUTED_RGB.2, + ), + text_body: Color::Rgb( + SHORELINE_TEXT_BODY_RGB.0, + SHORELINE_TEXT_BODY_RGB.1, + SHORELINE_TEXT_BODY_RGB.2, + ), + text_soft: Color::Rgb( + SHORELINE_TEXT_SOFT_RGB.0, + SHORELINE_TEXT_SOFT_RGB.1, + SHORELINE_TEXT_SOFT_RGB.2, + ), + border: Color::Rgb( + SHORELINE_BORDER_RGB.0, + SHORELINE_BORDER_RGB.1, + SHORELINE_BORDER_RGB.2, + ), + accent_primary: Color::Rgb( + SHORELINE_ACTION_RGB.0, + SHORELINE_ACTION_RGB.1, + SHORELINE_ACTION_RGB.2, + ), + accent_secondary: Color::Rgb( + SHORELINE_LIVE_RGB.0, + SHORELINE_LIVE_RGB.1, + SHORELINE_LIVE_RGB.2, + ), + accent_action: Color::Rgb( + SHORELINE_HUMAN_RGB.0, + SHORELINE_HUMAN_RGB.1, + SHORELINE_HUMAN_RGB.2, + ), + error_fg: Color::Rgb( + SHORELINE_ERROR_RGB.0, + SHORELINE_ERROR_RGB.1, + SHORELINE_ERROR_RGB.2, + ), + error_hover: Color::Rgb( + SHORELINE_ERROR_HOVER_RGB.0, + SHORELINE_ERROR_HOVER_RGB.1, + SHORELINE_ERROR_HOVER_RGB.2, + ), + error_surface: Color::Rgb( + SHORELINE_ERROR_SURFACE_RGB.0, + SHORELINE_ERROR_SURFACE_RGB.1, + SHORELINE_ERROR_SURFACE_RGB.2, + ), + error_border: Color::Rgb( + SHORELINE_ERROR_RGB.0, + SHORELINE_ERROR_RGB.1, + SHORELINE_ERROR_RGB.2, + ), + error_text: Color::Rgb( + SHORELINE_ERROR_TEXT_RGB.0, + SHORELINE_ERROR_TEXT_RGB.1, + SHORELINE_ERROR_TEXT_RGB.2, + ), + warning: Color::Rgb( + SHORELINE_WARNING_RGB.0, + SHORELINE_WARNING_RGB.1, + SHORELINE_WARNING_RGB.2, + ), + success: Color::Rgb( + SHORELINE_SUCCESS_RGB.0, + SHORELINE_SUCCESS_RGB.1, + SHORELINE_SUCCESS_RGB.2, + ), + info: Color::Rgb( + SHORELINE_ACTION_RGB.0, + SHORELINE_ACTION_RGB.1, + SHORELINE_ACTION_RGB.2, + ), + mode_agent: Color::Rgb( + SHORELINE_MODE_AGENT_RGB.0, + SHORELINE_MODE_AGENT_RGB.1, + SHORELINE_MODE_AGENT_RGB.2, + ), + mode_yolo: Color::Rgb( + SHORELINE_MODE_YOLO_RGB.0, + SHORELINE_MODE_YOLO_RGB.1, + SHORELINE_MODE_YOLO_RGB.2, + ), + mode_plan: Color::Rgb( + SHORELINE_MODE_PLAN_RGB.0, + SHORELINE_MODE_PLAN_RGB.1, + SHORELINE_MODE_PLAN_RGB.2, + ), + mode_operate: Color::Rgb( + SHORELINE_MODE_OPERATE_RGB.0, + SHORELINE_MODE_OPERATE_RGB.1, + SHORELINE_MODE_OPERATE_RGB.2, + ), + permission_ask: Color::Rgb( + SHORELINE_TEXT_MUTED_RGB.0, + SHORELINE_TEXT_MUTED_RGB.1, + SHORELINE_TEXT_MUTED_RGB.2, + ), + permission_auto_review: Color::Rgb( + SHORELINE_HUMAN_RGB.0, + SHORELINE_HUMAN_RGB.1, + SHORELINE_HUMAN_RGB.2, + ), + permission_full_access: Color::Rgb( + SHORELINE_WARNING_RGB.0, + SHORELINE_WARNING_RGB.1, + SHORELINE_WARNING_RGB.2, + ), + status_ready: Color::Rgb( + SHORELINE_SUCCESS_RGB.0, + SHORELINE_SUCCESS_RGB.1, + SHORELINE_SUCCESS_RGB.2, + ), + status_working: Color::Rgb( + SHORELINE_STATUS_WORKING_RGB.0, + SHORELINE_STATUS_WORKING_RGB.1, + SHORELINE_STATUS_WORKING_RGB.2, + ), + status_warning: Color::Rgb( + SHORELINE_WARNING_RGB.0, + SHORELINE_WARNING_RGB.1, + SHORELINE_WARNING_RGB.2, + ), + diff_added_fg: Color::Rgb( + SHORELINE_DIFF_ADDED_FG_RGB.0, + SHORELINE_DIFF_ADDED_FG_RGB.1, + SHORELINE_DIFF_ADDED_FG_RGB.2, + ), + diff_deleted_fg: Color::Rgb( + SHORELINE_DIFF_DELETED_FG_RGB.0, + SHORELINE_DIFF_DELETED_FG_RGB.1, + SHORELINE_DIFF_DELETED_FG_RGB.2, + ), + diff_added_bg: Color::Rgb( + SHORELINE_DIFF_ADDED_BG_RGB.0, + SHORELINE_DIFF_ADDED_BG_RGB.1, + SHORELINE_DIFF_ADDED_BG_RGB.2, + ), + diff_deleted_bg: Color::Rgb( + SHORELINE_DIFF_DELETED_BG_RGB.0, + SHORELINE_DIFF_DELETED_BG_RGB.1, + SHORELINE_DIFF_DELETED_BG_RGB.2, + ), + tool_running: Color::Rgb( + SHORELINE_LIVE_RGB.0, + SHORELINE_LIVE_RGB.1, + SHORELINE_LIVE_RGB.2, + ), + tool_success: Color::Rgb( + SHORELINE_SUCCESS_RGB.0, + SHORELINE_SUCCESS_RGB.1, + SHORELINE_SUCCESS_RGB.2, + ), + tool_failed: Color::Rgb( + SHORELINE_ERROR_RGB.0, + SHORELINE_ERROR_RGB.1, + SHORELINE_ERROR_RGB.2, + ), +}; + +/// Shoreline Light — Shoreline on warm paper, for light terminals. +pub const SHORELINE_LIGHT_UI_THEME: UiTheme = UiTheme { + name: "shoreline-light", + mode: PaletteMode::Light, + surface_bg: Color::Rgb( + SHORELINE_LIGHT_SURFACE_RGB.0, + SHORELINE_LIGHT_SURFACE_RGB.1, + SHORELINE_LIGHT_SURFACE_RGB.2, + ), + panel_bg: Color::Rgb( + SHORELINE_LIGHT_PANEL_RGB.0, + SHORELINE_LIGHT_PANEL_RGB.1, + SHORELINE_LIGHT_PANEL_RGB.2, + ), + elevated_bg: Color::Rgb( + SHORELINE_LIGHT_ELEVATED_RGB.0, + SHORELINE_LIGHT_ELEVATED_RGB.1, + SHORELINE_LIGHT_ELEVATED_RGB.2, + ), + composer_bg: Color::Rgb( + SHORELINE_LIGHT_COMPOSER_RGB.0, + SHORELINE_LIGHT_COMPOSER_RGB.1, + SHORELINE_LIGHT_COMPOSER_RGB.2, + ), + selection_bg: Color::Rgb( + SHORELINE_LIGHT_SELECTION_RGB.0, + SHORELINE_LIGHT_SELECTION_RGB.1, + SHORELINE_LIGHT_SELECTION_RGB.2, + ), + header_bg: Color::Rgb( + SHORELINE_LIGHT_CHROME_RGB.0, + SHORELINE_LIGHT_CHROME_RGB.1, + SHORELINE_LIGHT_CHROME_RGB.2, + ), + footer_bg: Color::Rgb( + SHORELINE_LIGHT_CHROME_RGB.0, + SHORELINE_LIGHT_CHROME_RGB.1, + SHORELINE_LIGHT_CHROME_RGB.2, + ), + text_dim: Color::Rgb( + SHORELINE_LIGHT_TEXT_DIM_RGB.0, + SHORELINE_LIGHT_TEXT_DIM_RGB.1, + SHORELINE_LIGHT_TEXT_DIM_RGB.2, + ), + text_hint: Color::Rgb( + SHORELINE_LIGHT_TEXT_HINT_RGB.0, + SHORELINE_LIGHT_TEXT_HINT_RGB.1, + SHORELINE_LIGHT_TEXT_HINT_RGB.2, + ), + text_muted: Color::Rgb( + SHORELINE_LIGHT_TEXT_MUTED_RGB.0, + SHORELINE_LIGHT_TEXT_MUTED_RGB.1, + SHORELINE_LIGHT_TEXT_MUTED_RGB.2, + ), + text_body: Color::Rgb( + SHORELINE_LIGHT_TEXT_BODY_RGB.0, + SHORELINE_LIGHT_TEXT_BODY_RGB.1, + SHORELINE_LIGHT_TEXT_BODY_RGB.2, + ), + text_soft: Color::Rgb( + SHORELINE_LIGHT_TEXT_SOFT_RGB.0, + SHORELINE_LIGHT_TEXT_SOFT_RGB.1, + SHORELINE_LIGHT_TEXT_SOFT_RGB.2, + ), + border: Color::Rgb( + SHORELINE_LIGHT_BORDER_RGB.0, + SHORELINE_LIGHT_BORDER_RGB.1, + SHORELINE_LIGHT_BORDER_RGB.2, + ), + accent_primary: Color::Rgb( + SHORELINE_LIGHT_ACTION_RGB.0, + SHORELINE_LIGHT_ACTION_RGB.1, + SHORELINE_LIGHT_ACTION_RGB.2, + ), + accent_secondary: Color::Rgb( + SHORELINE_LIGHT_LIVE_RGB.0, + SHORELINE_LIGHT_LIVE_RGB.1, + SHORELINE_LIGHT_LIVE_RGB.2, + ), + accent_action: Color::Rgb( + SHORELINE_LIGHT_HUMAN_RGB.0, + SHORELINE_LIGHT_HUMAN_RGB.1, + SHORELINE_LIGHT_HUMAN_RGB.2, + ), + error_fg: Color::Rgb( + SHORELINE_LIGHT_ERROR_RGB.0, + SHORELINE_LIGHT_ERROR_RGB.1, + SHORELINE_LIGHT_ERROR_RGB.2, + ), + error_hover: Color::Rgb( + SHORELINE_LIGHT_ERROR_RGB.0, + SHORELINE_LIGHT_ERROR_RGB.1, + SHORELINE_LIGHT_ERROR_RGB.2, + ), + error_surface: Color::Rgb( + SHORELINE_LIGHT_ERROR_SURFACE_RGB.0, + SHORELINE_LIGHT_ERROR_SURFACE_RGB.1, + SHORELINE_LIGHT_ERROR_SURFACE_RGB.2, + ), + error_border: Color::Rgb( + SHORELINE_LIGHT_ERROR_RGB.0, + SHORELINE_LIGHT_ERROR_RGB.1, + SHORELINE_LIGHT_ERROR_RGB.2, + ), + error_text: Color::Rgb( + SHORELINE_LIGHT_ERROR_TEXT_RGB.0, + SHORELINE_LIGHT_ERROR_TEXT_RGB.1, + SHORELINE_LIGHT_ERROR_TEXT_RGB.2, + ), + warning: Color::Rgb( + SHORELINE_LIGHT_WARNING_RGB.0, + SHORELINE_LIGHT_WARNING_RGB.1, + SHORELINE_LIGHT_WARNING_RGB.2, + ), + success: Color::Rgb( + SHORELINE_LIGHT_SUCCESS_RGB.0, + SHORELINE_LIGHT_SUCCESS_RGB.1, + SHORELINE_LIGHT_SUCCESS_RGB.2, + ), + info: Color::Rgb( + SHORELINE_LIGHT_ACTION_RGB.0, + SHORELINE_LIGHT_ACTION_RGB.1, + SHORELINE_LIGHT_ACTION_RGB.2, + ), + mode_agent: Color::Rgb( + SHORELINE_LIGHT_MODE_AGENT_RGB.0, + SHORELINE_LIGHT_MODE_AGENT_RGB.1, + SHORELINE_LIGHT_MODE_AGENT_RGB.2, + ), + mode_yolo: Color::Rgb( + SHORELINE_LIGHT_MODE_YOLO_RGB.0, + SHORELINE_LIGHT_MODE_YOLO_RGB.1, + SHORELINE_LIGHT_MODE_YOLO_RGB.2, + ), + mode_plan: Color::Rgb( + SHORELINE_LIGHT_MODE_PLAN_RGB.0, + SHORELINE_LIGHT_MODE_PLAN_RGB.1, + SHORELINE_LIGHT_MODE_PLAN_RGB.2, + ), + mode_operate: Color::Rgb( + SHORELINE_LIGHT_MODE_OPERATE_RGB.0, + SHORELINE_LIGHT_MODE_OPERATE_RGB.1, + SHORELINE_LIGHT_MODE_OPERATE_RGB.2, + ), + permission_ask: Color::Rgb( + SHORELINE_LIGHT_TEXT_MUTED_RGB.0, + SHORELINE_LIGHT_TEXT_MUTED_RGB.1, + SHORELINE_LIGHT_TEXT_MUTED_RGB.2, + ), + permission_auto_review: Color::Rgb( + SHORELINE_LIGHT_HUMAN_RGB.0, + SHORELINE_LIGHT_HUMAN_RGB.1, + SHORELINE_LIGHT_HUMAN_RGB.2, + ), + permission_full_access: Color::Rgb( + SHORELINE_LIGHT_WARNING_RGB.0, + SHORELINE_LIGHT_WARNING_RGB.1, + SHORELINE_LIGHT_WARNING_RGB.2, + ), + status_ready: Color::Rgb( + SHORELINE_LIGHT_SUCCESS_RGB.0, + SHORELINE_LIGHT_SUCCESS_RGB.1, + SHORELINE_LIGHT_SUCCESS_RGB.2, + ), + status_working: Color::Rgb( + SHORELINE_LIGHT_LIVE_RGB.0, + SHORELINE_LIGHT_LIVE_RGB.1, + SHORELINE_LIGHT_LIVE_RGB.2, + ), + status_warning: Color::Rgb( + SHORELINE_LIGHT_WARNING_RGB.0, + SHORELINE_LIGHT_WARNING_RGB.1, + SHORELINE_LIGHT_WARNING_RGB.2, + ), + diff_added_fg: Color::Rgb( + SHORELINE_LIGHT_DIFF_ADDED_FG_RGB.0, + SHORELINE_LIGHT_DIFF_ADDED_FG_RGB.1, + SHORELINE_LIGHT_DIFF_ADDED_FG_RGB.2, + ), + diff_deleted_fg: Color::Rgb( + SHORELINE_LIGHT_DIFF_DELETED_FG_RGB.0, + SHORELINE_LIGHT_DIFF_DELETED_FG_RGB.1, + SHORELINE_LIGHT_DIFF_DELETED_FG_RGB.2, + ), + diff_added_bg: Color::Rgb( + SHORELINE_LIGHT_DIFF_ADDED_BG_RGB.0, + SHORELINE_LIGHT_DIFF_ADDED_BG_RGB.1, + SHORELINE_LIGHT_DIFF_ADDED_BG_RGB.2, + ), + diff_deleted_bg: Color::Rgb( + SHORELINE_LIGHT_DIFF_DELETED_BG_RGB.0, + SHORELINE_LIGHT_DIFF_DELETED_BG_RGB.1, + SHORELINE_LIGHT_DIFF_DELETED_BG_RGB.2, + ), + tool_running: Color::Rgb( + SHORELINE_LIGHT_LIVE_RGB.0, + SHORELINE_LIGHT_LIVE_RGB.1, + SHORELINE_LIGHT_LIVE_RGB.2, + ), + tool_success: Color::Rgb( + SHORELINE_LIGHT_SUCCESS_RGB.0, + SHORELINE_LIGHT_SUCCESS_RGB.1, + SHORELINE_LIGHT_SUCCESS_RGB.2, + ), + tool_failed: Color::Rgb( + SHORELINE_LIGHT_ERROR_RGB.0, + SHORELINE_LIGHT_ERROR_RGB.1, + SHORELINE_LIGHT_ERROR_RGB.2, + ), +}; + pub const SOLARIZED_LIGHT_UI_THEME: UiTheme = UiTheme { name: "solarized-light", mode: PaletteMode::SolarizedLight, @@ -888,6 +1325,8 @@ pub const UWU_UI_THEME: UiTheme = UiTheme { pub enum ThemeId { System, Terminal, + Shoreline, + ShorelineLight, Underwater, UnderwaterRetro, Whale, @@ -914,6 +1353,8 @@ impl ThemeId { "terminal" => Some(Self::Terminal), "underwater" | "deepsea" => Some(Self::Underwater), "underwater-retro" | "retro" => Some(Self::UnderwaterRetro), + "shoreline" => Some(Self::Shoreline), + "shoreline-light" => Some(Self::ShorelineLight), "dark" => Some(Self::Whale), "light" => Some(Self::WhaleLight), "grayscale" => Some(Self::Grayscale), @@ -936,6 +1377,8 @@ impl ThemeId { match self { Self::System => "system", Self::Terminal => "terminal", + Self::Shoreline => "shoreline", + Self::ShorelineLight => "shoreline-light", Self::Underwater => "underwater", Self::UnderwaterRetro => "underwater-retro", Self::Whale => "dark", @@ -958,6 +1401,8 @@ impl ThemeId { match self { Self::System => "System", Self::Terminal => "Terminal", + Self::Shoreline => "Shoreline", + Self::ShorelineLight => "Shoreline Light", Self::Underwater => "Underwater", Self::UnderwaterRetro => "Underwater Retro", Self::Whale => "Blue Stage", @@ -980,6 +1425,8 @@ impl ThemeId { match self { Self::System => "Follow terminal background (COLORFGBG / macOS appearance)", Self::Terminal => "Inherit terminal colors fully (transparent surfaces, ANSI accents)", + Self::Shoreline => "Warm charcoal, one restrained blue — the desktop client's palette", + Self::ShorelineLight => "Shoreline on warm paper — the desktop client's light mode", Self::Underwater => "The painted ocean field: ombre water, ambient life, the whale", Self::UnderwaterRetro => "Flat phosphor-teal ocean: the legacy deepsea look, no ombre", Self::Whale => "Stage black, action blue, and one Signal Gold human beacon", @@ -1007,6 +1454,8 @@ impl ThemeId { match self { Self::System => UiTheme::detect(), Self::Terminal => TERMINAL_UI_THEME, + Self::Shoreline => SHORELINE_UI_THEME, + Self::ShorelineLight => SHORELINE_LIGHT_UI_THEME, Self::Underwater => UNDERWATER_UI_THEME, Self::UnderwaterRetro => UNDERWATER_RETRO_UI_THEME, Self::Whale => UI_THEME, @@ -1028,6 +1477,8 @@ impl ThemeId { pub const SELECTABLE_THEMES: &[ThemeId] = &[ ThemeId::System, ThemeId::Terminal, + ThemeId::Shoreline, + ThemeId::ShorelineLight, ThemeId::Underwater, ThemeId::UnderwaterRetro, ThemeId::Whale, @@ -1090,6 +1541,8 @@ pub fn normalize_theme_name(value: &str) -> Option<&'static str> { "terminal" | "term" | "transparent" | "follow-terminal" | "inherit" => Some("terminal"), "underwater" | "deepsea" | "deep-sea" | "ocean" | "ombre" => Some("underwater"), "underwater-retro" | "retro" | "uw-retro" => Some("underwater-retro"), + "shoreline" | "warm" | "charcoal" => Some("shoreline"), + "shoreline-light" | "paper" => Some("shoreline-light"), "dark" | "whale" | "whale-dark" => Some("dark"), "light" | "whale-light" => Some("light"), "grayscale" | "greyscale" | "gray" | "grey" | "mono" | "monochrome" | "black-white" @@ -1165,6 +1618,8 @@ mod tests { [ "system", "terminal", + "shoreline", + "shoreline-light", "underwater", "underwater-retro", "dark", diff --git a/crates/palette/src/tokens.rs b/crates/palette/src/tokens.rs index e65112c355..d4c04dc782 100644 --- a/crates/palette/src/tokens.rs +++ b/crates/palette/src/tokens.rs @@ -222,6 +222,84 @@ pub const MATRIX_TEXT_SOFT_RGB: (u8, u8, u8) = (221, 255, 221); // #DDFFDD pub const MATRIX_TEXT_DIM_RGB: (u8, u8, u8) = (0, 108, 0); // #006C00, lifted for 3:1 pub const MATRIX_BORDER_RGB: (u8, u8, u8) = (0, 204, 0); // #00CC00 +// Shoreline — the product-client palette, shared with the GPUI desktop. +// +// Warm charcoal ground and warm paper sheet, one restrained blue, and the +// whale's ivory ink on both sides. This is the default shell from 0.9.14: +// the navy "Underwater" water column remains a selectable theme, it is no +// longer the ground the product opens on. Values are the GPUI client's +// `Theme` slots (`codehwhale-gpui/src/workspace/mod.rs`), so the terminal +// and the desktop window are one product rather than two visual systems. +// +// Every pair audited by `contrast::theme_contrast_violations` clears its +// floor: body roles clear 4.5:1 on all four surfaces, hint/dim and the +// status roles clear 3:1. +pub const SHORELINE_SURFACE_RGB: (u8, u8, u8) = (33, 31, 35); // #211F23 warm charcoal field +pub const SHORELINE_PANEL_RGB: (u8, u8, u8) = (43, 40, 46); // #2B282E raised plate +pub const SHORELINE_ELEVATED_RGB: (u8, u8, u8) = (53, 49, 58); // #35313A +pub const SHORELINE_COMPOSER_RGB: (u8, u8, u8) = (43, 40, 46); // #2B282E +pub const SHORELINE_CHROME_RGB: (u8, u8, u8) = (26, 24, 28); // #1A181C recessed chrome +pub const SHORELINE_SELECTION_RGB: (u8, u8, u8) = (53, 73, 103); // #354967 +pub const SHORELINE_TEXT_BODY_RGB: (u8, u8, u8) = (242, 236, 229); // #F2ECE5 +pub const SHORELINE_TEXT_SOFT_RGB: (u8, u8, u8) = (217, 210, 220); // #D9D2DC +pub const SHORELINE_TEXT_MUTED_RGB: (u8, u8, u8) = (176, 167, 178); // #B0A7B2 +pub const SHORELINE_TEXT_HINT_RGB: (u8, u8, u8) = (154, 145, 159); // #9A919F +pub const SHORELINE_TEXT_DIM_RGB: (u8, u8, u8) = (126, 117, 131); // #7E7583 +pub const SHORELINE_BORDER_RGB: (u8, u8, u8) = (73, 66, 77); // #49424D +pub const SHORELINE_ACTION_RGB: (u8, u8, u8) = (144, 185, 255); // #90B9FF the one blue +pub const SHORELINE_LIVE_RGB: (u8, u8, u8) = (127, 214, 198); // #7FD6C6 the live lane +pub const SHORELINE_HUMAN_RGB: (u8, u8, u8) = (246, 196, 83); // #F6C453 Signal Gold, the human lane +pub const SHORELINE_ERROR_RGB: (u8, u8, u8) = (255, 143, 168); // #FF8FA8 +pub const SHORELINE_ERROR_HOVER_RGB: (u8, u8, u8) = (255, 166, 187); // #FFA6BB +pub const SHORELINE_ERROR_SURFACE_RGB: (u8, u8, u8) = (58, 32, 41); // #3A2029 +pub const SHORELINE_ERROR_TEXT_RGB: (u8, u8, u8) = (255, 220, 230); // #FFDCE6 +pub const SHORELINE_WARNING_RGB: (u8, u8, u8) = (240, 168, 104); // #F0A868 +pub const SHORELINE_SUCCESS_RGB: (u8, u8, u8) = (163, 217, 119); // #A3D977 +pub const SHORELINE_STATUS_WORKING_RGB: (u8, u8, u8) = SHORELINE_LIVE_RGB; +// Mode badges keep the shipped mode ramp: these four are proof-tested distinct +// from one another and from every semantic lane, and a mode badge is identity, +// not decoration. `mode_agent` must differ from `accent_primary` or +// `adapt::theme_semantic_foreground_role` resolves the action lane to a mode +// role (`ModeAgent`) before it ever reaches `Action`. +pub const SHORELINE_MODE_AGENT_RGB: (u8, u8, u8) = (126, 180, 232); // #7EB4E8 +pub const SHORELINE_MODE_YOLO_RGB: (u8, u8, u8) = (255, 112, 160); // #FF70A0 +pub const SHORELINE_MODE_PLAN_RGB: (u8, u8, u8) = (185, 220, 236); // #B9DCEC +pub const SHORELINE_MODE_OPERATE_RGB: (u8, u8, u8) = (173, 136, 255); // #AD88FF +pub const SHORELINE_DIFF_ADDED_FG_RGB: (u8, u8, u8) = (127, 214, 160); // #7FD6A0 +pub const SHORELINE_DIFF_ADDED_BG_RGB: (u8, u8, u8) = (30, 44, 37); // #1E2C25 +pub const SHORELINE_DIFF_DELETED_FG_RGB: (u8, u8, u8) = (255, 154, 171); // #FF9AAB +pub const SHORELINE_DIFF_DELETED_BG_RGB: (u8, u8, u8) = (51, 32, 42); // #33202A + +// Shoreline Light — the same system on warm paper. +pub const SHORELINE_LIGHT_SURFACE_RGB: (u8, u8, u8) = (245, 240, 233); // #F5F0E9 +pub const SHORELINE_LIGHT_PANEL_RGB: (u8, u8, u8) = (236, 229, 224); // #ECE5E0 +pub const SHORELINE_LIGHT_ELEVATED_RGB: (u8, u8, u8) = (255, 252, 247); // #FFFCF7 +pub const SHORELINE_LIGHT_COMPOSER_RGB: (u8, u8, u8) = (251, 247, 241); // #FBF7F1 +pub const SHORELINE_LIGHT_CHROME_RGB: (u8, u8, u8) = (237, 231, 224); // #EDE7E0 +pub const SHORELINE_LIGHT_SELECTION_RGB: (u8, u8, u8) = (200, 217, 249); // #C8D9F9 +pub const SHORELINE_LIGHT_TEXT_BODY_RGB: (u8, u8, u8) = (48, 40, 50); // #302832 +pub const SHORELINE_LIGHT_TEXT_SOFT_RGB: (u8, u8, u8) = (74, 65, 76); // #4A414C +pub const SHORELINE_LIGHT_TEXT_MUTED_RGB: (u8, u8, u8) = (107, 96, 110); // #6B606E +pub const SHORELINE_LIGHT_TEXT_HINT_RGB: (u8, u8, u8) = (117, 112, 128); // #757080 +pub const SHORELINE_LIGHT_TEXT_DIM_RGB: (u8, u8, u8) = (138, 130, 144); // #8A8290 +pub const SHORELINE_LIGHT_BORDER_RGB: (u8, u8, u8) = (215, 206, 213); // #D7CED5 +pub const SHORELINE_LIGHT_ACTION_RGB: (u8, u8, u8) = (36, 91, 199); // #245BC7 +pub const SHORELINE_LIGHT_LIVE_RGB: (u8, u8, u8) = (31, 122, 107); // #1F7A6B +pub const SHORELINE_LIGHT_HUMAN_RGB: (u8, u8, u8) = (122, 85, 0); // #7A5500 Signal Gold at AA on paper +pub const SHORELINE_LIGHT_ERROR_RGB: (u8, u8, u8) = (180, 35, 90); // #B4235A +pub const SHORELINE_LIGHT_ERROR_SURFACE_RGB: (u8, u8, u8) = (251, 228, 236); // #FBE4EC +pub const SHORELINE_LIGHT_ERROR_TEXT_RGB: (u8, u8, u8) = (122, 18, 53); // #7A1235 +pub const SHORELINE_LIGHT_WARNING_RGB: (u8, u8, u8) = (143, 85, 20); // #8F5514 +pub const SHORELINE_LIGHT_SUCCESS_RGB: (u8, u8, u8) = (47, 107, 58); // #2F6B3A +pub const SHORELINE_LIGHT_MODE_AGENT_RGB: (u8, u8, u8) = (22, 54, 178); // #1636B2 distinct from LIGHT action +pub const SHORELINE_LIGHT_MODE_YOLO_RGB: (u8, u8, u8) = (181, 35, 90); // #B5235A +pub const SHORELINE_LIGHT_MODE_PLAN_RGB: (u8, u8, u8) = (52, 92, 128); // #345C80 +pub const SHORELINE_LIGHT_MODE_OPERATE_RGB: (u8, u8, u8) = (112, 71, 184); // #7047B8 +pub const SHORELINE_LIGHT_DIFF_ADDED_FG_RGB: (u8, u8, u8) = (31, 107, 69); // #1F6B45 +pub const SHORELINE_LIGHT_DIFF_ADDED_BG_RGB: (u8, u8, u8) = (226, 242, 230); // #E2F2E6 +pub const SHORELINE_LIGHT_DIFF_DELETED_FG_RGB: (u8, u8, u8) = (168, 40, 80); // #A82850 +pub const SHORELINE_LIGHT_DIFF_DELETED_BG_RGB: (u8, u8, u8) = (251, 230, 236); // #FBE6EC + // Semantic colors pub const BORDER_COLOR_RGB: (u8, u8, u8) = WHALE_BORDER_RGB; diff --git a/crates/tui/src/runtime_web/codewhale-192.png b/crates/tui/src/runtime_web/codewhale-192.png index bfb67a6ff0..7513605aee 100644 Binary files a/crates/tui/src/runtime_web/codewhale-192.png and b/crates/tui/src/runtime_web/codewhale-192.png differ diff --git a/crates/tui/src/settings.rs b/crates/tui/src/settings.rs index fb167e29fa..a4f94d8e72 100644 --- a/crates/tui/src/settings.rs +++ b/crates/tui/src/settings.rs @@ -345,13 +345,14 @@ pub struct Settings { /// ca, de, fr, id, hi, ru, uk. /// Every shipped pack holds full `en.json` parity; nothing falls back. pub locale: String, - /// Named UI theme. `"underwater"` is the fresh-install default and paints - /// the ocean field. `"terminal"` fully inherits the host terminal's - /// foreground/background. `"system"`, `"dark"`, `"light"`, - /// `"grayscale"`, and the community presets: `"catppuccin-mocha"`, - /// `"tokyo-night"`, `"dracula"`, `"gruvbox-dark"`. The - /// `background_color` setting still overrides the surface color on top - /// of the resolved theme. + /// Named UI theme. `"shoreline"` is the fresh-install default: the warm + /// charcoal and restrained blue the desktop client paints too. + /// `"underwater"` paints the ocean field. `"terminal"` fully inherits the + /// host terminal's foreground/background. `"system"`, `"dark"`, + /// `"light"`, `"grayscale"`, and the community presets: + /// `"catppuccin-mocha"`, `"tokyo-night"`, `"dracula"`, + /// `"gruvbox-dark"`. The `background_color` setting still overrides the + /// surface color on top of the resolved theme. pub theme: String, /// Optional main TUI background color as a 6-digit hex RGB value. pub background_color: Option, @@ -571,7 +572,7 @@ impl Default for Settings { show_tool_details: false, inline_diffs: "full".to_string(), locale: "auto".to_string(), - theme: "underwater".to_string(), + theme: "shoreline".to_string(), background_color: None, composer_density: "comfortable".to_string(), composer_border: true, @@ -2886,9 +2887,9 @@ fn normalize_synchronized_output(value: &str) -> &str { fn normalize_settings_theme(value: &str) -> String { // A malformed persisted selector must not turn into a painted application - // background. Falling back to the underwater default keeps a single - // compiled first-party theme until the user picks an explicit palette. - normalize_theme_setting(value).unwrap_or_else(|_| "underwater".to_string()) + // background. Falling back to the compiled default keeps a single + // first-party theme until the user picks an explicit palette. + normalize_theme_setting(value).unwrap_or_else(|_| "shoreline".to_string()) } /// Returns `true` when the active terminal is Ptyxis (the new default @@ -4032,22 +4033,22 @@ mod tests { } #[test] - fn default_settings_resolve_to_the_underwater_theme() { - // Slice C: the fresh-install default is the underwater theme, end to - // end from `Settings::default()` through theme resolution. + fn default_settings_resolve_to_the_shoreline_theme() { + // The fresh-install default is the Shoreline theme, end to end from + // `Settings::default()` through theme resolution. let settings = Settings::default(); - assert_eq!(settings.theme, "underwater"); + assert_eq!(settings.theme, "shoreline"); let (name, id, theme) = codewhale_palette::resolve_theme_setting(&settings.theme, None) .expect("default resolves"); - assert_eq!(id, codewhale_palette::ThemeId::Underwater); - assert_eq!(name, "underwater"); - assert_eq!(theme.name, "underwater"); + assert_eq!(id, codewhale_palette::ThemeId::Shoreline); + assert_eq!(name, "shoreline"); + assert_eq!(theme.name, "shoreline"); } #[test] fn theme_normalizes_supported_values_and_rejects_unknowns() { let mut settings = Settings::default(); - assert_eq!(settings.theme, "underwater"); + assert_eq!(settings.theme, "shoreline"); settings.set("theme", "grayscale").expect("set grayscale"); assert_eq!(settings.theme, "grayscale"); @@ -5482,7 +5483,7 @@ mod tests { let loaded = Settings::load().expect("load settings"); assert_eq!( - loaded.theme, "underwater", + loaded.theme, "shoreline", "explicit CODEWHALE_HOME must not inherit ambient legacy settings" ); assert_eq!( diff --git a/crates/tui/src/tui/goldens/config_panel_120x32.txt b/crates/tui/src/tui/goldens/config_panel_120x32.txt index 761b64a452..1893d4eb8a 100644 --- a/crates/tui/src/tui/goldens/config_panel_120x32.txt +++ b/crates/tui/src/tui/goldens/config_panel_120x32.txt @@ -15,16 +15,16 @@ │ Help Expand Groups Off [ ] │kind choice │ Contextual tips On [x] │available not observed this session │ Pin Last Prompt On [x] │ - │ Tool detail level Off [ ] │system | terminal | underwater | - │ Inline file changes Full diff ‹ › │underwater-retro | dark | light | - │ Output pacing auto ‹ › │grayscale | catppuccin-mocha | - │ Cost currency usd ‹ › │tokyo-night | dracula | gruvbox-dark | - │ Transcript spacing comfortable ‹ › │claude | matrix | solarized-light | uwu - │ Tool cards compact ‹ › │Enter or click again: Enter opens + │ Tool detail level Off [ ] │system | terminal | shoreline | + │ Inline file changes Full diff ‹ › │shoreline-light | underwater | + │ Output pacing auto ‹ › │underwater-retro | dark | light | + │ Cost currency usd ‹ › │grayscale | catppuccin-mocha | + │ Transcript spacing comfortable ‹ › │tokyo-night | dracula | gruvbox-dark | + │ Tool cards compact ‹ › │claude | matrix | solarized-light | uwu + │ │Enter or click again: Enter opens │ │choices │ │ - │ │ - system | terminal | underwater | underwater-retro | dark | light | grayscale | catppuccin-mocha | tokyo-night |… + system | terminal | shoreline | shoreline-light | underwater | underwater-retro | dark | light | grayscale |… Preview: ▶▶ ask · agent type=filter, Up/Down=select, Enter=edit, Esc=close diff --git a/crates/tui/src/tui/goldens/config_panel_80x24.txt b/crates/tui/src/tui/goldens/config_panel_80x24.txt index b28af81691..c4ce3e0ad5 100644 --- a/crates/tui/src/tui/goldens/config_panel_80x24.txt +++ b/crates/tui/src/tui/goldens/config_panel_80x24.txt @@ -14,7 +14,7 @@ Reasoning background highlight On [x] SAVED │ Help Expand Groups Off [ ] SAVED │ Contextual tips On [x] SAVED │ - system | terminal | underwater | underwater-retro | dark | light |… + system | terminal | shoreline | shoreline-light | underwater |… Enter or click again: Enter opens choices · Theme: current terminal · saved terminal · applies on save Preview: ▶▶ ask · agent diff --git a/crates/tui/src/tui/goldens/edit_theme_120x32.txt b/crates/tui/src/tui/goldens/edit_theme_120x32.txt index 043e5798c9..1d8eb23629 100644 --- a/crates/tui/src/tui/goldens/edit_theme_120x32.txt +++ b/crates/tui/src/tui/goldens/edit_theme_120x32.txt @@ -4,26 +4,26 @@ Edit Theme [theme] Scope: SAVED - Current: underwater + Current: shoreline Choose: 1. system 2. terminal - ▸ 3. underwater - 4. underwater-retro - 5. dark - 6. light - 7. grayscale - 8. catppuccin-mocha - 9. tokyo-night - 10. dracula - 11. gruvbox-dark - 12. claude - 13. matrix - 14. solarized-light - 15. uwu - - + ▸ 3. shoreline + 4. shoreline-light + 5. underwater + 6. underwater-retro + 7. dark + 8. light + 9. grayscale + 10. catppuccin-mocha + 11. tokyo-night + 12. dracula + 13. gruvbox-dark + 14. claude + 15. matrix + 16. solarized-light + 17. uwu [ Apply ] [ Cancel ] ↑/↓ or click choose · Enter/Apply · Esc/Cancel · 1-9 jump diff --git a/crates/tui/src/tui/goldens/edit_theme_80x24.txt b/crates/tui/src/tui/goldens/edit_theme_80x24.txt index b1bdf46b88..e13d606cb6 100644 --- a/crates/tui/src/tui/goldens/edit_theme_80x24.txt +++ b/crates/tui/src/tui/goldens/edit_theme_80x24.txt @@ -4,19 +4,19 @@ Edit Theme [theme] Scope: SAVED - Current: underwater + Current: shoreline Choose: 1. system 2. terminal - ▸ 3. underwater - 4. underwater-retro - 5. dark - 6. light - 7. grayscale - 8. catppuccin-mocha - 9. tokyo-night - 10. dracula + ▸ 3. shoreline + 4. shoreline-light + 5. underwater + 6. underwater-retro + 7. dark + 8. light + 9. grayscale + 10. catppuccin-mocha [ Apply ] [ Cancel ] ↑/↓ or click choose · Enter/Apply · Esc/Cancel · 1-9 jump diff --git a/crates/tui/src/tui/goldens/settings_100x30.txt b/crates/tui/src/tui/goldens/settings_100x30.txt index daac23af3c..c38e7a5a7c 100644 --- a/crates/tui/src/tui/goldens/settings_100x30.txt +++ b/crates/tui/src/tui/goldens/settings_100x30.txt @@ -1,10 +1,12 @@ ▸ Appearance System PREVIEW · Blue Stage Models & providers Terminal ▎ restyle the work stage - Work Underwater ├── whale-1 · footer band - Tools & MCP Underwater Retro └── whale-2 · goldens - Trust [ ✓ Blue Stage ] ● working whale-1 editing · 14:41:02 × - Motion Blue Stage Light ✓ done whale-2 surfaced ✓ · 14:39:02 × - Advanced Grayscale done: stage restyled + Work Shoreline ├── whale-1 · footer band + Tools & MCP Shoreline Light └── whale-2 · goldens + Trust Underwater ● working whale-1 editing · 14:41:02 × + Motion Underwater Retro ✓ done whale-2 surfaced ✓ · 14:39:02 × + Advanced [ ✓ Blue Stage ] done: stage restyled + Blue Stage Light + Grayscale Catppuccin Mocha Tokyo Night Dracula @@ -16,8 +18,6 @@ MOTION (OPTIONAL) ○ low motion ◉ ambient life - - ● working ○ ready ✓ done ! cauti FLEET LEDGER WHALE │ASSIGNMENT │STATE diff --git a/crates/tui/src/tui/goldens/settings_120x32.txt b/crates/tui/src/tui/goldens/settings_120x32.txt index 3b6bb3036f..e2d0390cc3 100644 --- a/crates/tui/src/tui/goldens/settings_120x32.txt +++ b/crates/tui/src/tui/goldens/settings_120x32.txt @@ -1,10 +1,12 @@ ▸ Appearance System PREVIEW · Blue Stage Models & providers Terminal ▎ restyle the work stage - Work Underwater ├── whale-1 · footer band - Tools & MCP Underwater Retro └── whale-2 · goldens - Trust [ ✓ Blue Stage ] ● working whale-1 editing · 14:41:02 ×12 - Motion Blue Stage Light ✓ done whale-2 surfaced ✓ · 14:39:02 ×34 - Advanced Grayscale done: stage restyled + Work Shoreline ├── whale-1 · footer band + Tools & MCP Shoreline Light └── whale-2 · goldens + Trust Underwater ● working whale-1 editing · 14:41:02 ×12 + Motion Underwater Retro ✓ done whale-2 surfaced ✓ · 14:39:02 ×34 + Advanced [ ✓ Blue Stage ] done: stage restyled + Blue Stage Light + Grayscale Catppuccin Mocha Tokyo Night Dracula @@ -18,8 +20,6 @@ ◉ ambient life - - ● working ○ ready ✓ done ! caution ✗ f FLEET LEDGER WHALE │ASSIGNMENT │STATE diff --git a/crates/tui/src/tui/goldens/settings_160x40.txt b/crates/tui/src/tui/goldens/settings_160x40.txt index 6341b484df..61e78a6cbc 100644 --- a/crates/tui/src/tui/goldens/settings_160x40.txt +++ b/crates/tui/src/tui/goldens/settings_160x40.txt @@ -1,10 +1,12 @@ ▸ Appearance System PREVIEW · Blue Stage Models & providers Terminal ▎ restyle the work stage - Work Underwater ├── whale-1 · footer band - Tools & MCP Underwater Retro └── whale-2 · goldens - Trust [ ✓ Blue Stage ] ● working whale-1 editing · 14:41:02 ×12 - Motion Blue Stage Light ✓ done whale-2 surfaced ✓ · 14:39:02 ×34 - Advanced Grayscale done: stage restyled + Work Shoreline ├── whale-1 · footer band + Tools & MCP Shoreline Light └── whale-2 · goldens + Trust Underwater ● working whale-1 editing · 14:41:02 ×12 + Motion Underwater Retro ✓ done whale-2 surfaced ✓ · 14:39:02 ×34 + Advanced [ ✓ Blue Stage ] done: stage restyled + Blue Stage Light + Grayscale Catppuccin Mocha Tokyo Night Dracula @@ -26,8 +28,6 @@ - - ● working ○ ready ✓ done ! caution ✗ failed FLEET LEDGER WHALE │ASSIGNMENT │STATE diff --git a/crates/tui/src/tui/goldens/settings_80x24.txt b/crates/tui/src/tui/goldens/settings_80x24.txt index 5bd549168f..b815ce44cb 100644 --- a/crates/tui/src/tui/goldens/settings_80x24.txt +++ b/crates/tui/src/tui/goldens/settings_80x24.txt @@ -1,6 +1,8 @@ Appearance Models & providers Work Tools & MCP Trust Motion › System Terminal + Shoreline + Shoreline Light Underwater Underwater Retro [ ✓ Blue Stage ] diff --git a/crates/tui/src/tui/goldens/theme_picker_120x32.txt b/crates/tui/src/tui/goldens/theme_picker_120x32.txt index 17ad567471..6ac9c53a45 100644 --- a/crates/tui/src/tui/goldens/theme_picker_120x32.txt +++ b/crates/tui/src/tui/goldens/theme_picker_120x32.txt @@ -4,21 +4,21 @@ 1. System Follow terminal background (COLORFGBG / macOS appearance) 2. Terminal Inherit terminal colors fully (transparent surfaces, ANSI accents) - ▸ 3. Underwater The painted ocean field: ombre water, ambient life, the whale - 4. Underwater Retro Flat phosphor-teal ocean: the legacy deepsea look, no ombre - 5. Blue Stage Stage black, action blue, and one Signal Gold human beacon - 6. Blue Stage Light Paper, cobalt action, and one Signal Gold human beacon - 7. Grayscale Color-minimal high contrast - 8. Catppuccin Mocha Soft pastels on warm dark - 9. Tokyo Night Deep blue/violet night palette - 10. Dracula Classic high-contrast purple - 11. Gruvbox Dark Vintage warm earth tones - 12. Claude Warm navy & coral - 13. Matrix The Matrix films inspired theme - 14. Solarized Light Solarized light — Light, calming palette on warm ivory — easy on the eyes - 15. Uwu Soft kawaii night — sakura, mint, and peach - - + 3. Shoreline Warm charcoal, one restrained blue — the desktop client's palette + 4. Shoreline Light Shoreline on warm paper — the desktop client's light mode + ▸ 5. Underwater The painted ocean field: ombre water, ambient life, the whale + 6. Underwater Retro Flat phosphor-teal ocean: the legacy deepsea look, no ombre + 7. Blue Stage Stage black, action blue, and one Signal Gold human beacon + 8. Blue Stage Light Paper, cobalt action, and one Signal Gold human beacon + 9. Grayscale Color-minimal high contrast + 10. Catppuccin Mocha Soft pastels on warm dark + 11. Tokyo Night Deep blue/violet night palette + 12. Dracula Classic high-contrast purple + 13. Gruvbox Dark Vintage warm earth tones + 14. Claude Warm navy & coral + 15. Matrix The Matrix films inspired theme + 16. Solarized Light Solarized light — Light, calming palette on warm ivory — easy on the eyes + 17. Uwu Soft kawaii night — sakura, mint, and peach diff --git a/crates/tui/src/tui/goldens/theme_picker_80x24.txt b/crates/tui/src/tui/goldens/theme_picker_80x24.txt index ddbf11dd0a..584fc1ec1a 100644 --- a/crates/tui/src/tui/goldens/theme_picker_80x24.txt +++ b/crates/tui/src/tui/goldens/theme_picker_80x24.txt @@ -1,23 +1,23 @@ theme · live preview ──────────────────────────────────────────────────────── - - 1. System Follow terminal background… - 2. Terminal Inherit terminal colors fully… - ▸ 3. Underwater The painted ocean field: ombre… - 4. Underwater Retro Flat phosphor-teal ocean: the… - 5. Blue Stage Stage black, action blue, and one… - 6. Blue Stage Light Paper, cobalt action, and one… - 7. Grayscale Color-minimal high contrast - 8. Catppuccin Mocha Soft pastels on warm dark - 9. Tokyo Night Deep blue/violet night palette - 10. Dracula Classic high-contrast purple - 11. Gruvbox Dark Vintage warm earth tones - 12. Claude Warm navy & coral - 13. Matrix The Matrix films inspired theme - 14. Solarized Light Solarized light — Light, calming… - 15. Uwu Soft kawaii night — sakura, mint,… - + █ + 1. System Follow terminal background… █ + 2. Terminal Inherit terminal colors fully… █ + 3. Shoreline Warm charcoal, one restrained… █ + 4. Shoreline Light Shoreline on warm paper — the… █ + ▸ 5. Underwater The painted ocean field: ombre… █ + 6. Underwater Retro Flat phosphor-teal ocean: the… █ + 7. Blue Stage Stage black, action blue, and one… █ + 8. Blue Stage Light Paper, cobalt action, and one… █ + 9. Grayscale Color-minimal high contrast █ + 10. Catppuccin Mocha Soft pastels on warm dark █ + 11. Tokyo Night Deep blue/violet night palette █ + 12. Dracula Classic high-contrast purple █ + 13. Gruvbox Dark Vintage warm earth tones █ + 14. Claude Warm navy & coral █ + 15. Matrix The Matrix films inspired theme █ + 16. Solarized Light Solarized light — Light, calming… │ ↑/↓ preview Enter save Esc revert ────────────────────────────────────────────────────────────────────────────── diff --git a/crates/tui/src/tui/theme_picker.rs b/crates/tui/src/tui/theme_picker.rs index 5a25fa6840..e0eee8db0e 100644 --- a/crates/tui/src/tui/theme_picker.rs +++ b/crates/tui/src/tui/theme_picker.rs @@ -226,9 +226,9 @@ fn theme_options_with_custom( .help("Pick a theme with live preview") .values(SettingValues::new( Cow::Owned(current.clone()), - // A reset returns to the underwater default, not a + // A reset returns to the Shoreline default, not a // detected palette that can repaint it. - Cow::Borrowed("underwater"), + Cow::Borrowed("shoreline"), Cow::Borrowed(name), )) .availability(SettingAvailability::Available) @@ -255,7 +255,7 @@ fn theme_options_with_custom( .help("Pick a user-authored theme overlay") .values(SettingValues::new( Cow::Owned(current.clone()), - Cow::Borrowed("underwater"), + Cow::Borrowed("shoreline"), Cow::Owned(custom.selector.clone()), )) .availability(SettingAvailability::Available) @@ -612,11 +612,11 @@ mod tests { #[test] fn enter_commits_with_persist_true() { let mut v = ThemePickerView::new("system".to_string()); - v.handle_key(key(KeyCode::Char('8'))); // -> CatppuccinMocha + v.handle_key(key(KeyCode::Char('9'))); // -> Grayscale let action = v.handle_key(key(KeyCode::Enter)); match action { ViewAction::EmitAndClose(ViewEvent::ThemeSelectionUpdated { theme, persist }) => { - assert_eq!(theme, ThemeId::CatppuccinMocha.name()); + assert_eq!(theme, ThemeId::Grayscale.name()); assert!(persist); } other => panic!("expected commit, got {other:?}"), @@ -706,11 +706,11 @@ mod tests { } #[test] - fn digit_jumps_to_underwater_and_previews() { + fn digit_jumps_to_shoreline_and_previews() { let mut v = ThemePickerView::new("system".to_string()); let action = v.handle_key(key(KeyCode::Char('3'))); - // Underwater follows System and Terminal. - assert_eq!(selected_values(&action), Some(("underwater", false))); + // Shoreline follows System and Terminal. + assert_eq!(selected_values(&action), Some(("shoreline", false))); } #[test] diff --git a/crates/tui/src/tui/theme_picker/tideline_tests.rs b/crates/tui/src/tui/theme_picker/tideline_tests.rs index 3357e996f3..e747925b05 100644 --- a/crates/tui/src/tui/theme_picker/tideline_tests.rs +++ b/crates/tui/src/tui/theme_picker/tideline_tests.rs @@ -7,8 +7,8 @@ use crate::tui::golden_harness::render_golden_text; use codewhale_palette::SELECTABLE_THEMES; #[test] -fn theme_rows_are_the_fourteen_selectable_themes() { - assert_eq!(tideline_theme_rows().len(), 15); +fn theme_rows_are_the_seventeen_selectable_themes() { + assert_eq!(tideline_theme_rows().len(), 17); assert_eq!(tideline_theme_rows().as_slice(), SELECTABLE_THEMES); } diff --git a/crates/tui/src/tui/views/mod.rs b/crates/tui/src/tui/views/mod.rs index 6a8880ed99..3740d17b3e 100644 --- a/crates/tui/src/tui/views/mod.rs +++ b/crates/tui/src/tui/views/mod.rs @@ -10404,7 +10404,7 @@ context_window = 262144 "Enter must open the theme editor" ); - // ↓ highlights underwater: preview (persist:false), editor stays open. + // ↓ highlights shoreline: preview (persist:false), editor stays open. match key(&mut view, KeyCode::Down) { ViewAction::Emit(ViewEvent::ConfigUpdated { key, @@ -10412,7 +10412,7 @@ context_window = 262144 persist, }) => { assert_eq!(key, "theme"); - assert_eq!(value, "underwater"); + assert_eq!(value, "shoreline"); assert!(!persist, "highlighting must not persist"); } other => panic!("highlight must preview, got {other:?}"), @@ -10454,7 +10454,7 @@ context_window = 262144 persist, }) => { assert_eq!(key, "theme"); - assert_eq!(value, "underwater"); + assert_eq!(value, "shoreline"); assert!(persist, "Apply must persist"); } other => panic!("enter must persist the highlight, got {other:?}"), @@ -10529,14 +10529,14 @@ context_window = 262144 modifiers: KeyModifiers::NONE, }) }; - // Choice index 2 is underwater (system, terminal, underwater, …). + // Choice index 2 is shoreline (system, terminal, shoreline, …). let (rect, _) = view .last_choice_hitboxes .borrow() .iter() .copied() .find(|(_, idx)| *idx == 2) - .expect("rendered underwater hitbox"); + .expect("rendered shoreline hitbox"); match hover(&mut view, rect.x, rect.y) { ViewAction::Emit(ViewEvent::ConfigUpdated { key, @@ -10544,7 +10544,7 @@ context_window = 262144 persist, }) => { assert_eq!(key, "theme"); - assert_eq!(value, "underwater"); + assert_eq!(value, "shoreline"); assert!(!persist, "hover preview must not persist"); } other => panic!("hover must preview, got {other:?}"), diff --git a/crates/tui/src/tui/views/tideline_tests.rs b/crates/tui/src/tui/views/tideline_tests.rs index 6cecf07724..003ac63a7a 100644 --- a/crates/tui/src/tui/views/tideline_tests.rs +++ b/crates/tui/src/tui/views/tideline_tests.rs @@ -29,7 +29,9 @@ fn draw_stage(width: u16, height: u16) -> String { ascii_safe: false, locale: Locale::En, }; - let theme_list = TidelineThemeList::new(&UI_THEME, 4).motion(false, true); + // Index 6 is Blue Stage (Blue Stage follows Shoreline, Shoreline + // Light, Underwater and Underwater Retro), matching `candidate_name`. + let theme_list = TidelineThemeList::new(&UI_THEME, 6).motion(false, true); let preview = TidelineSettingsPreview { active_theme: &UI_THEME, candidate: &UI_THEME, @@ -126,23 +128,24 @@ fn settings_strip_windows_to_the_selected_category_with_painted_hitboxes() { } #[test] -fn theme_list_shows_fourteen_themes_boxed_selection_and_motion_toggles() { - let list = TidelineThemeList::new(&UI_THEME, 4).motion(false, true); +fn theme_list_shows_seventeen_themes_boxed_selection_and_motion_toggles() { + let list = TidelineThemeList::new(&UI_THEME, 6).motion(false, true); let text = render_golden_text(30, 24, |buf| { render_tideline_theme_list(Rect::new(0, 0, 30, 24), buf, &list); }); - assert_eq!(SELECTABLE_THEMES.len(), 15, "4 mode rows + 11 presets"); + assert_eq!(SELECTABLE_THEMES.len(), 17, "4 mode rows + 13 presets"); for name in [ "System", "Terminal", + "Shoreline", "Blue Stage", "Catppuccin Mocha", "Uwu", ] { assert!(text.contains(name), "missing {name}: {text}"); } - // Index 4 is Blue Stage; Underwater + Underwater Retro sit between - // Terminal and Blue Stage. + // Index 6 is Blue Stage; Shoreline, Shoreline Light, Underwater and + // Underwater Retro sit between Terminal and Blue Stage. assert!(text.contains("Underwater"), "{text}"); assert!(text.contains("Underwater Retro"), "{text}"); assert!( @@ -237,7 +240,7 @@ fn settings_rail_and_theme_list_hitboxes_match_painted_rows() { let mut buf = Buffer::empty(Rect::new(0, 0, w, h)); render_tideline_theme_list(form, &mut buf, &list); let boxes = tideline_theme_list_hitboxes(form, &list); - assert_eq!(boxes.len(), 17, "15 theme rows + 2 motion toggles"); + assert_eq!(boxes.len(), 19, "17 theme rows + 2 motion toggles"); for rect in &boxes { let cells: String = (rect.x..rect.x + rect.width) .map(|x| buf[(x, rect.y)].symbol().to_string()) diff --git a/docs/design/TUI_REDESIGN.md b/docs/design/TUI_REDESIGN.md new file mode 100644 index 0000000000..829b07b25b --- /dev/null +++ b/docs/design/TUI_REDESIGN.md @@ -0,0 +1,164 @@ +# TUI redesign — Shoreline + +Branch `tui-redesign`, 2026-09-15. The Codewhale TUI re-inks to the product +client's palette and moves toward the conventions the terminal-agent market +converged on. The whalemark is unchanged and is the constant across every +surface. + +Two asks produced this. First: a more professional shell, closer to what the +market ships. Second: closer to the GPUI client, which is deliberately simple. +Both are the same move — `codehwhale-gpui` already *is* the product client, and +it already solved "calm, warm, one accent". The terminal should read as the same +product, not as a second design system. + +## What changed + +### 1. One palette, two clients (landed) + +`SHORELINE_UI_THEME` and `SHORELINE_LIGHT_UI_THEME` in +`crates/palette/src/themes.rs` cite the same slots +`codehwhale-gpui/src/workspace/mod.rs:147-164` installs into GPUI's `Theme`. +The terminal's default was `underwater` — a saturated navy gradient with +decorative ambient life. It is a named theme now, not the ground the product +opens on. + +| Role | Value | Note | +| --- | --- | --- | +| field (`surface_bg`) | `#211F23` | warm charcoal | +| plate (`panel_bg`, `composer_bg`) | `#2B282E` | raised | +| chrome (`header_bg`, `footer_bg`) | `#1A181C` | recessed | +| border | `#49424D` | | +| body | `#F2ECE5` | the whale's ivory survives | +| soft / muted / hint / dim | `#D9D2DC` / `#B0A7B2` / `#9A919F` / `#7E7583` | 4.5:1 floors | +| action, selection | `#90B9FF` / `#354967` | the one blue | +| live | `#7FD6C6` | | +| human | `#F6C453` | Signal Gold, unchanged | +| warning / danger / success | `#F0A868` / `#FF8FA8` / `#A3D977` | | +| mode ramp | `#7EB4E8` / `#B9DCEC` / `#AD88FF` / `#FF70A0` | the shipped ramp | + +Light is the same system on warm paper (`#F5F0E9` field, `#245BC7` action). + +Three mechanisms make it hold: + +- **`adapt::theme_remap_active`** lists both new presets, so every direct + `palette::TEXT_*` / `WHALE_*` call site lands on a Shoreline slot instead of + a navy-tuned value. Without this the re-ink would be half-applied — the + frame would repaint while every widget kept its cold grey. +- **The mode ramp is separate from `accent_primary`.** `mode_agent` must not + equal the action blue: `adapt::theme_semantic_foreground_role` resolves mode + slots *before* the action lane, so an equal value reads the action lane as + `ModeAgent` and the ANSI-16 role matrix collapses. Found by + `every_selectable_theme_keeps_mode_badges_distinct`; fixed in the theme, not + the test. +- **Every audited pair clears its floor.** `contrast::theme_contrast_violations` + runs over `SELECTABLE_THEMES`, so the new presets are held to 4.5:1 on the + four surfaces and 3:1 for hint/dim/status/diff. + +`scripts/export-design-tokens.py` reads only `(WHALE|LIGHT)_*_RGB`, so the new +`SHORELINE_*` constants deliberately leave `web/app/tokens.css` untouched — +`--check` still passes. When the web surfaces are re-inked, that pattern is the +one line to widen. + +### 2. What the market actually does (surveyed, not assumed) + +Seven of nine reference checkouts under `refs/` are terminal TUIs (codex, +opencode, kimi-code, grokbuild, oh-my-pi, piagent, prime-agent); `dsh` and +`openhands` are not TUIs at all. The convergent rules: + +1. **No persistent top bar.** 7/7. Branding is launch-only — codex's session + card and grokbuild's cwd bar are the first transcript cell, they scroll + away. +2. **Exactly one status row, never two, never above the composer.** All five + that have a status line. +3. **That row carries model + cwd + git branch + context % + cost.** +4. **Full-width composer.** 7/7. No reading-column cap — this is where the + terminal convention and the desktop window legitimately differ, and the + terminal wins here. +5. **Assistant turn = bare markdown on the field.** User turn = a filled + background or one coloured glyph, never bold text alone. +6. **Tool calls are collapsible cards or one-line headers**, not bare lines. +7. **One accent over a near-black field.** `#141414` recurs as the base field + in three separate projects. + +Against that list the TUI is already close on 1, 4, 5 and 6 — `work_surface::height` +returns 0 unless a panel is explicitly open, the composer is full width, and +tool receipts are typed cells. The live deviations are 2 and 3, and the launch +screen's proportions. + +### 3. Deliberately not done + +- **The composer stays full width.** The GPUI client centres a 690px column; + no terminal agent does. Narrowing it here would be the desktop's layout + applied where the medium disagrees. +- **The ocean is not deleted.** `underwater` keeps its water column, ombre and + ambient life. It is selectable, and `/theme` lists it. Nothing is lost. + +## Remaining work + +### The one-status-row merge (highest value, not started) + +Two rows sit under the composer: + +- slot 6, the posture bar — `frame.rs:1820-1835` → + `phase_strip::tideline_footer_from_app` (`phase_strip.rs:1270`) → + `render_tideline_footer` (`phase_strip.rs:953`). +- slot 7, the metrics line — `frame.rs:1841-1844` → `render_info_row` + (`frame.rs:387`), fed by `info_segments` (`frame.rs:61`). + +They are separate systems with separate shed ladders, separate hitbox +registries (`register_footer_count_targets`, `register_info_interaction_targets`) +and separate golden families, so this is a merge, not a deletion. + +The shape that fits the existing design: **the footer becomes the one row**, +gaining a route segment and a cost segment. `TidelineFooterFacts` already +carries `context_percent` (it uses it for the ≥80% cap warning), and +`phase_strip::route_identity_fields` — which the info line already calls — is +the shared route formatter, so both segments are one call each. `metrics_line` +then defaults to `ChromeRowPreset::Hidden` and the transcript gets the line +back. + +**The blocker to solve first, not after:** the metrics line owns two pointer +targets — `/model` on the route segment and the context inspector on the meter. +Hiding the row without re-homing them removes two interactions users have +today. The footer's count rects are the precedent to copy: paint the segment, +return its `Rect`, register the target. Do that in the same commit that +hides the row, or the slice ships a regression. + +### The launch screen + +`underwater::launch_empty_state` (`underwater.rs:1948`) rides the hero mark +(14×6, `MarkSize::Large`) beside three text lines and then a session card. Two +things read as splash rather than product: the mark is unindented at the top +left, and its six rows are taller than the text column beside it, so `New +session` floats in the mark's dead space. The GPUI launch is a centred compact +lockup — mark, wordmark, one heading, one line of subtitle. Worth doing; the +golden churn is contained to the launch surfaces. + +### Adjacent, out of scope here + +- Two `main` test failures pre-date this branch and are unrelated: + `runtime_api::tests::set_config_rejects_unknown_key_with_bad_request` + (the code says `unknown setting`, the test expects `unknown config key` — + both present at `4a85cb7877`) and + `tui::views::tests::every_settings_row_reaches_a_store` + (`auto_compact_threshold_percent` is a schema row `Settings::set` never + accepted; also present at `4a85cb7877`). +- `arrow_navigation_wraps_at_picker_edges` and + `theme_picker_uses_shared_settings_controller` are order-dependent under a + bare `cargo test`: they read the real user theme directory, so a + `custom:` row changes the last picker row. Run tests through + `scripts/dev-test.sh`, which supplies the hermetic HOME. +- `crates/tui/src/tui/work_surface/tideline.rs:207-263` (`tideline_rail_groups`, + the RUNS/WHALES/FLEET/WORK/CONTEXT groups) is `#[allow(dead_code)]` + translation scaffolding. The live rail paints `RailPanel` dock tabs. It + should be deleted, not reimplemented. + +## Verification + +Goldens record cell symbols only — `golden_harness.rs:27-49` dumps +`cell.symbol()`, so **a palette change is invisible to every golden buffer**. +The `ink plane` the harness documents at `:84-94` would close that hole and is +documented but unimplemented. Until it exists, palette regressions are caught +by the contrast audit and by a real terminal capture, not by a golden. + +What was run, with counts, is recorded in the three commits on this branch. diff --git a/extensions/vscode/media/codewhale.svg b/extensions/vscode/media/codewhale.svg index a082e2bc5b..f67e8d277b 100644 --- a/extensions/vscode/media/codewhale.svg +++ b/extensions/vscode/media/codewhale.svg @@ -1,11 +1,3 @@ - - + diff --git a/scripts/brand/trace-brand.py b/scripts/brand/trace-brand.py deleted file mode 100644 index 054f3d5c92..0000000000 --- a/scripts/brand/trace-brand.py +++ /dev/null @@ -1,233 +0,0 @@ -#!/usr/bin/env python3 -"""Re-derive every brand surface from the founder's artwork in brand/. - - python3 scripts/brand/trace-brand.py # rewrite every output - python3 scripts/brand/trace-brand.py --check # exit 1 if any output drifted - -Founder sources (committed, never regenerated): - - brand/codewhalemarkfinal.png 1254x1254 sheet; hero mark top centre - brand/wordmark0901.png "codewhale" wordmark, navy on white, 2172x724 - brand/wordmarkinverted.png the same wordmark, white on navy, 2508x627 - -`brand/mark.svg` is the kept trace of the sheet's hero mark (viewBox 0 0 512 -512, one evenodd path, fill currentColor). It is the source of truth for the -mark: this script does not re-trace it, it derives every other mark surface -from it. The wordmark is traced from `wordmark0901.png` every run: - - magick brand/wordmark0901.png -colorspace Gray -threshold 60% -trim +repage wm.pbm - potrace wm.pbm -s --flat -t 20 -O 0.4 -a 1.2 -o wm-raw.svg - -potrace's `translate(0,H) scale(0.1,-0.1)` group is folded into the -coordinates so the result is one compact path in a tight pixel viewBox -(1874x264 for the 0901 render; the wordmark is ~7.1:1). - -`wordmarkinverted.png` traces just as cleanly (negate first, threshold 40%), -but its render is set slightly tighter (2172x310 after trim, ~7.0:1), so the -0901 render is the one path used for both colourways: geometry that differs -between the light and dark wordmark is a second wordmark. - -Outputs: - - brand/wordmark.svg #142352 on transparent - brand/wordmark-inverted.svg #FFFFFF on transparent, same path - brand/mark-navy.svg mark.svg path, #142352 - brand/mark-gradient.svg mark.svg path, cobalt->action gradient - web/public/brand/{mark,mark-gradient,wordmark,wordmark-inverted}.svg - web/app/icon.svg white mark on the #142352 rounded tile - web/app/favicon.ico 16/32/48 of the tile - web/app/apple-icon.png 180, opaque tile - web/public/icon-192.png, icon-512.png - -`web/components/whale.tsx` inlines the mark path; the script checks it still -matches `brand/mark.svg` rather than rewriting a React component. - -Needs ImageMagick 7 (`magick`) and potrace on PATH. No network. -""" - -from __future__ import annotations - -import re -import shutil -import subprocess -import sys -import tempfile -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -BRAND = ROOT / "brand" -WEB = ROOT / "web" - -NAVY = "#142352" -WHITE = "#FFFFFF" -# Gradient stops are the TUI's WHALE_COBALT -> WHALE_ACTION. -GRADIENT = ("#1535B2", "#6AA6DC") - -# The mark in the 512 tile: the sheet's app icon sets the whale at ~2/3 of -# the tile width, so scale the 512 mark by 0.72 and centre its content box -# (x 16.9..494.3, y 46.7..471.1) on 256,256. -TILE_SCALE = 0.72 -TILE_TRANSLATE = (72, 70) -TILE_RADIUS = 102.4 # 20% of 512, the sheet's corner - -THRESHOLD = "60%" -POTRACE = ["potrace", "-s", "--flat", "-t", "20", "-O", "0.4", "-a", "1.2"] - - -def run(*args: str) -> None: - subprocess.run(args, check=True) - - -def number(value: float) -> str: - text = f"{value:.1f}" - return text[:-2] if text.endswith(".0") else text - - -def fold_potrace(svg: str) -> tuple[str, float, float]: - """Return (path d, width, height) with potrace's group transform applied.""" - width = float(re.search(r'width="([\d.]+)pt"', svg).group(1)) - height = float(re.search(r'height="([\d.]+)pt"', svg).group(1)) - tx, ty, sx, sy = ( - float(v) - for v in re.search( - r"translate\(([-\d.]+),([-\d.]+)\) scale\(([-\d.]+),([-\d.]+)\)", svg - ).groups() - ) - d = " ".join(re.findall(r' tuple[str, float, float]: - pbm = tmp / "wm.pbm" - raw = tmp / "wm-raw.svg" - run( - "magick", str(png), "-colorspace", "Gray", "-threshold", THRESHOLD, - "-trim", "+repage", str(pbm), - ) - run(*POTRACE, str(pbm), "-o", str(raw)) - return fold_potrace(raw.read_text()) - - -def wordmark_svg(d: str, width: float, height: float, fill: str) -> str: - return ( - f'' - f'\n' - ) - - -def mark_path() -> str: - return re.search(r' d="([^"]+)"', (BRAND / "mark.svg").read_text()).group(1) - - -def mark_svg(fill: str, defs: str = "") -> str: - return ( - '' - f'{defs}\n' - ) - - -def icon_svg() -> str: - tx, ty = TILE_TRANSLATE - return ( - '' - f'' - f'\n' - ) - - -def rasters(icon: Path) -> None: - run( - "magick", "-background", "none", str(icon), - "-define", "icon:auto-resize=48,32,16", str(WEB / "app/favicon.ico"), - ) - run( - "magick", "-background", NAVY, str(icon), "-resize", "180x180", - "-alpha", "off", "-depth", "8", str(WEB / "app/apple-icon.png"), - ) - for size in (192, 512): - run( - "magick", "-background", "none", str(icon), "-resize", f"{size}x{size}", - "-depth", "8", str(WEB / f"public/icon-{size}.png"), - ) - - -def main() -> int: - check = "--check" in sys.argv[1:] - gradient = ( - '' - f'' - f'' - ) - with tempfile.TemporaryDirectory() as tmpdir: - tmp = Path(tmpdir) - d, width, height = trace_wordmark(BRAND / "wordmark0901.png", tmp) - texts = { - BRAND / "wordmark.svg": wordmark_svg(d, width, height, NAVY), - BRAND / "wordmark-inverted.svg": wordmark_svg(d, width, height, WHITE), - BRAND / "mark-navy.svg": mark_svg(NAVY), - BRAND / "mark-gradient.svg": mark_svg("url(#mark-gradient)", gradient), - WEB / "app/icon.svg": icon_svg(), - } - texts[WEB / "public/brand/mark.svg"] = (BRAND / "mark.svg").read_text() - for name in ("mark-gradient", "wordmark", "wordmark-inverted"): - texts[WEB / "public/brand" / f"{name}.svg"] = texts[BRAND / f"{name}.svg"] - - whale = (WEB / "components/whale.tsx").read_text() - inline = re.search(r'WHALE_MARK =\s*"([^"]+)"', whale).group(1) - drift = [] if inline == mark_path() else ["web/components/whale.tsx"] - - if check: - drift += [ - str(path.relative_to(ROOT)) - for path, text in texts.items() - if not path.exists() or path.read_text() != text - ] - for name in drift: - print(f"drift: {name}", file=sys.stderr) - return 1 if drift else 0 - - for path, text in texts.items(): - path.write_text(text) - print(f"wrote {path.relative_to(ROOT)}") - rasters(WEB / "app/icon.svg") - print("wrote web/app/favicon.ico apple-icon.png public/icon-192.png icon-512.png") - for name in drift: - print(f"drift: {name} no longer matches brand/mark.svg", file=sys.stderr) - return 1 if drift else 0 - - -if __name__ == "__main__": - if not shutil.which("magick") or not shutil.which("potrace"): - sys.exit("needs `magick` (ImageMagick 7) and `potrace` on PATH") - sys.exit(main()) diff --git a/web/app/opengraph-image.tsx b/web/app/opengraph-image.tsx index d99791d8d4..2df331ca77 100644 --- a/web/app/opengraph-image.tsx +++ b/web/app/opengraph-image.tsx @@ -14,7 +14,7 @@ export const contentType = "image/png"; // regeneration on the site fail with a 500 and serve its build snapshot forever. async function brandSvgs(): Promise<[string, string]> { const [mark, wordmark] = await Promise.all([ - readFile(join(process.cwd(), "public/brand/mark.svg")), + readFile(join(process.cwd(), "public/brand/mark-reversed.svg")), readFile(join(process.cwd(), "public/brand/wordmark-inverted.svg")), ]); return [mark.toString().replace("currentColor", "#ffffff"), wordmark.toString()]; @@ -43,8 +43,8 @@ export default async function OpengraphImage() { }} > - {/* The traced wordmark is 1874x264 (~7.1:1). */} - Codewhale + {/* The family wordmark is 1024x160 (6.4:1). */} + Codewhale
{IDENTITY_PHRASE}
diff --git a/web/lib/public-auth-routes.ts b/web/lib/public-auth-routes.ts index 97d45e7548..dbe4f9b2d6 100644 --- a/web/lib/public-auth-routes.ts +++ b/web/lib/public-auth-routes.ts @@ -14,8 +14,9 @@ import { pathLocale } from "./i18n/path"; /** * App-icon raster (1254×1254) used on public account-entry pages: the canonical * mark in the current brand treatment — blue gradient glyph on a white rounded - * tile, generated from repo-root-relative `web/public/brand/mark.svg`. The vector is - * the master — never hand-edit this PNG, regenerate it. Pinned by hash so a + * tile. The master vectors are the canonical family in + * `codewhale-apps/packages/brand/svg/` (byte copies in `web/public/brand/`) — + * never hand-edit this PNG, regenerate it from the family. Pinned by hash so a * redrawn or stale substitute fails the test rather than shipping. */ export const CANONICAL_MARK_SRC = "/brand/codewhale-mark.png"; diff --git a/web/public/brand/mark-gradient.svg b/web/public/brand/mark-gradient.svg index e7458e956e..412e4c14d9 100644 --- a/web/public/brand/mark-gradient.svg +++ b/web/public/brand/mark-gradient.svg @@ -1 +1,9 @@ - + + + + + + + + + diff --git a/web/public/brand/mark-mono.svg b/web/public/brand/mark-mono.svg new file mode 100644 index 0000000000..f67e8d277b --- /dev/null +++ b/web/public/brand/mark-mono.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/public/brand/mark-reversed.svg b/web/public/brand/mark-reversed.svg new file mode 100644 index 0000000000..90ceae43ce --- /dev/null +++ b/web/public/brand/mark-reversed.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/public/brand/mark.svg b/web/public/brand/mark.svg index 75b6a4d86d..71d3b534c6 100644 --- a/web/public/brand/mark.svg +++ b/web/public/brand/mark.svg @@ -1 +1,3 @@ - + + + diff --git a/web/public/brand/wordmark-inverted.svg b/web/public/brand/wordmark-inverted.svg index e537014177..b0bb87e19f 100644 --- a/web/public/brand/wordmark-inverted.svg +++ b/web/public/brand/wordmark-inverted.svg @@ -1 +1,3 @@ - + + + diff --git a/web/public/brand/wordmark.svg b/web/public/brand/wordmark.svg index 81133ab7e2..03d61e8957 100644 --- a/web/public/brand/wordmark.svg +++ b/web/public/brand/wordmark.svg @@ -1 +1,3 @@ - + + +