From 7508767014c04eb3a706eaf1efe64fe1f9baac44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sat, 9 May 2026 17:55:35 -0400 Subject: [PATCH] Replace sidebar header with workspace picker row The design's sidebar opens with an ELY title row plus a three-part workspace picker: a layout-grid tile (opens spaces settings), a glass pill showing the active space's emoji + name (cycles to next space on click), and a plus button (opens spaces settings). The previous header was a static title + space-name label that did not match the design. Cycle behaviour reuses the existing select_next_space core API to avoid introducing a new state flow; the existing SelectNextSpace shortcut delegates through the same helper. --- crates/ely_app/src/shell/chrome/mod.rs | 2 + .../src/shell/chrome/sidebar_header.rs | 201 ++++++++++++++++++ crates/ely_app/src/shell/render.rs | 36 +--- crates/ely_app/src/shell/spaces.rs | 4 + 4 files changed, 209 insertions(+), 34 deletions(-) create mode 100644 crates/ely_app/src/shell/chrome/sidebar_header.rs diff --git a/crates/ely_app/src/shell/chrome/mod.rs b/crates/ely_app/src/shell/chrome/mod.rs index 31db2d5..cb05aca 100644 --- a/crates/ely_app/src/shell/chrome/mod.rs +++ b/crates/ely_app/src/shell/chrome/mod.rs @@ -1,3 +1,5 @@ +pub(crate) mod sidebar_header; pub(crate) mod wallpaper; +pub(crate) use sidebar_header::render_sidebar_header; pub(crate) use wallpaper::{WallpaperTheme, render_wallpaper}; diff --git a/crates/ely_app/src/shell/chrome/sidebar_header.rs b/crates/ely_app/src/shell/chrome/sidebar_header.rs new file mode 100644 index 0000000..a5c74a7 --- /dev/null +++ b/crates/ely_app/src/shell/chrome/sidebar_header.rs @@ -0,0 +1,201 @@ +use ely_browser_core::BrowserSnapshot; +use ely_design_system::colors; +use ely_domain::Space; +use gpui::{ + AnyElement, BoxShadow, Context, InteractiveElement, IntoElement, ParentElement, SharedString, + StatefulInteractiveElement, Styled, div, hsla, linear_color_stop, linear_gradient, point, px, + rgb, rgba, +}; +use gpui_component::IconName; + +use crate::shell::ElyShell; + +pub(crate) fn render_sidebar_header( + snapshot: &BrowserSnapshot, + cx: &mut Context, +) -> AnyElement { + let active_space = snapshot + .spaces + .iter() + .find(|space| space.id() == &snapshot.active_space_id); + + div() + .flex() + .flex_col() + .pt(px(36.0)) + .px(px(10.0)) + .pb(px(10.0)) + .gap(px(8.0)) + .flex_shrink_0() + .child(render_title_row()) + .child(render_workspace_picker(active_space, cx)) + .into_any_element() +} + +fn render_title_row() -> AnyElement { + div() + .flex() + .items_center() + .gap(px(4.0)) + .pl(px(6.0)) + .child( + div() + .text_size(px(12.5)) + .font_weight(gpui::FontWeight(500.0)) + .text_color(rgb(colors::INK_2)) + .child("ELY Browser"), + ) + .child( + div() + .text_color(rgb(colors::INK_3)) + .opacity(0.6) + .child(IconName::ChevronDown), + ) + .into_any_element() +} + +fn render_workspace_picker( + active_space: Option<&Space>, + cx: &mut Context, +) -> AnyElement { + div() + .flex() + .items_center() + .gap(px(6.0)) + .py(px(4.0)) + .px(px(2.0)) + .child(render_workspaces_tile(cx)) + .child(render_picker_pill(active_space, cx)) + .child(render_add_workspace_button(cx)) + .into_any_element() +} + +fn render_workspaces_tile(cx: &mut Context) -> AnyElement { + div() + .id(SharedString::from("workspace-tile")) + .size(px(32.0)) + .rounded(px(9.0)) + .bg(linear_gradient( + 135.0, + linear_color_stop(hsla(0.0, 0.0, 1.0, 1.0), 0.0), + linear_color_stop(hsla(20.0 / 360.0, 0.6, 0.94, 1.0), 1.0), + )) + .shadow(soft_shadow()) + .flex() + .items_center() + .justify_center() + .text_color(rgb(colors::ACCENT)) + .cursor_pointer() + .hover(|style| style.opacity(0.85)) + .active(|style| style.opacity(0.7)) + .on_click(cx.listener(|shell, _, window, cx| { + shell.open_internal_tab("ely://settings/spaces", window, cx); + })) + .child(IconName::LayoutDashboard) + .into_any_element() +} + +fn render_picker_pill(active_space: Option<&Space>, cx: &mut Context) -> AnyElement { + let space_name = active_space + .map(|space| space.name().to_string()) + .unwrap_or_default(); + let space_glyph = active_space + .map(|space| space.icon().to_string()) + .unwrap_or_default(); + + div() + .id(SharedString::from("workspace-picker")) + .flex_1() + .min_w_0() + .flex() + .items_center() + .gap(px(8.0)) + .px(px(8.0)) + .py(px(6.0)) + .rounded(px(9.0)) + .bg(rgba(PICKER_BG)) + .shadow(soft_shadow()) + .cursor_pointer() + .hover(|style| style.bg(rgba(PICKER_BG_HOVER))) + .active(|style| style.opacity(0.85)) + .on_click(cx.listener(|shell, _, window, cx| { + shell.cycle_to_next_space(window, cx); + })) + .child(render_workspace_glyph(space_glyph)) + .child( + div() + .flex_1() + .min_w_0() + .truncate() + .text_size(px(13.0)) + .font_weight(gpui::FontWeight(500.0)) + .text_color(rgb(colors::INK)) + .child(space_name), + ) + .child( + div() + .text_color(rgb(colors::INK_3)) + .child(IconName::ChevronDown), + ) + .into_any_element() +} + +fn render_workspace_glyph(emoji: String) -> AnyElement { + div() + .size(px(18.0)) + .rounded(px(5.0)) + .bg(linear_gradient( + 135.0, + linear_color_stop(hsla(341.0 / 360.0, 0.78, 0.67, 1.0), 0.0), + linear_color_stop(hsla(15.0 / 360.0, 0.55, 0.53, 1.0), 1.0), + )) + .flex() + .items_center() + .justify_center() + .text_size(px(10.0)) + .text_color(rgb(0xffffff)) + .child(emoji) + .into_any_element() +} + +fn render_add_workspace_button(cx: &mut Context) -> AnyElement { + div() + .id(SharedString::from("workspace-add")) + .size(px(32.0)) + .rounded(px(9.0)) + .bg(rgba(ADD_BUTTON_BG)) + .shadow(soft_shadow()) + .flex() + .items_center() + .justify_center() + .text_color(rgb(colors::INK_3)) + .cursor_pointer() + .hover(|style| style.bg(rgba(PICKER_BG)).text_color(rgb(colors::INK))) + .active(|style| style.opacity(0.7)) + .on_click(cx.listener(|shell, _, window, cx| { + shell.open_internal_tab("ely://settings/spaces", window, cx); + })) + .child(IconName::Plus) + .into_any_element() +} + +const PICKER_BG: u32 = 0xffffff99; +const PICKER_BG_HOVER: u32 = 0xffffffd9; +const ADD_BUTTON_BG: u32 = 0xffffff66; + +fn soft_shadow() -> Vec { + vec![ + BoxShadow { + color: hsla(0.0, 0.0, 1.0, 0.7), + offset: point(px(0.0), px(1.0)), + blur_radius: px(0.0), + spread_radius: px(0.0), + }, + BoxShadow { + color: hsla(25.0 / 360.0, 0.33, 0.12, 0.08), + offset: point(px(0.0), px(0.0)), + blur_radius: px(0.0), + spread_radius: px(1.0), + }, + ] +} diff --git a/crates/ely_app/src/shell/render.rs b/crates/ely_app/src/shell/render.rs index cc99f7c..f86941e 100644 --- a/crates/ely_app/src/shell/render.rs +++ b/crates/ely_app/src/shell/render.rs @@ -13,7 +13,7 @@ use gpui_component::{ scroll::ScrollableElement, }; -use super::chrome::{WallpaperTheme, render_wallpaper}; +use super::chrome::{WallpaperTheme, render_sidebar_header, render_wallpaper}; use super::sidebar::{collapsed_sidebar_active, render_command_bar_identity}; use super::{ElyShell, ShellState, archive_labels::archive_detail_label}; @@ -213,7 +213,7 @@ impl ElyShell { .bg(rgba(PANEL_BG)) .shadow(panel_shadow()) .overflow_hidden() - .child(self.render_sidebar_header(snapshot)) + .child(render_sidebar_header(snapshot, cx)) .child( div() .flex_1() @@ -265,38 +265,6 @@ impl ElyShell { .into_any_element() } - fn render_sidebar_header(&self, snapshot: &BrowserSnapshot) -> AnyElement { - let space_name = snapshot - .spaces - .iter() - .find(|s| s.id() == &snapshot.active_space_id) - .map(|s| s.name().to_string()) - .unwrap_or_default(); - - div() - .pt(px(48.0)) - .px(px(14.0)) - .pb(px(10.0)) - .flex() - .flex_col() - .gap_1() - .flex_shrink_0() - .child( - div() - .text_size(px(15.0)) - .font_semibold() - .text_color(rgb(colors::INK)) - .child("ELY Browser"), - ) - .child( - div() - .text_size(px(11.0)) - .text_color(rgb(colors::INK_3)) - .child(space_name), - ) - .into_any_element() - } - fn render_sidebar_footer(&self, snapshot: &BrowserSnapshot) -> AnyElement { div() .px(px(14.0)) diff --git a/crates/ely_app/src/shell/spaces.rs b/crates/ely_app/src/shell/spaces.rs index 821eda3..93eb28d 100644 --- a/crates/ely_app/src/shell/spaces.rs +++ b/crates/ely_app/src/shell/spaces.rs @@ -87,6 +87,10 @@ impl ElyShell { window: &mut Window, cx: &mut Context, ) { + self.cycle_to_next_space(window, cx); + } + + pub(super) fn cycle_to_next_space(&mut self, window: &mut Window, cx: &mut Context) { if let ShellState::Ready(core) = &mut self.state && core.select_next_space().is_ok() {