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.
This commit is contained in:
2026-05-09 17:55:35 -04:00
parent dcd4bb4c48
commit 7508767014
4 changed files with 209 additions and 34 deletions
@@ -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<ElyShell>,
) -> 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<ElyShell>,
) -> 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<ElyShell>) -> 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<ElyShell>) -> 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<ElyShell>) -> 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<BoxShadow> {
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),
},
]
}