Add spaces settings page

This commit is contained in:
2026-05-08 02:55:37 -04:00
parent f6e2b1b96b
commit 3a1c415572
5 changed files with 282 additions and 0 deletions
@@ -12,6 +12,7 @@ mod settings;
mod shortcuts;
mod sidebar_tabs;
mod site_settings;
mod spaces;
mod sync;
mod task_manager;
@@ -50,6 +51,7 @@ impl ElyShell {
"ely://about" => self.render_about_page(snapshot),
"ely://settings" => self.render_settings_page(snapshot, cx),
"ely://settings/sidebar-tabs" => self.render_sidebar_tabs_page(snapshot, cx),
"ely://settings/spaces" => self.render_spaces_page(snapshot, cx),
"ely://settings/shortcuts" => self.render_shortcuts_page(snapshot),
"ely://settings/plugins" => self.render_plugins_page(snapshot, cx),
"ely://settings/profiles" => self.render_profiles_page(snapshot, cx),
@@ -24,6 +24,12 @@ const SETTINGS_ROUTES: &[SettingsRoute] = &[
detail: "Vertical tabs, pinned area, and auto archive policy.",
route: "ely://settings/sidebar-tabs",
},
SettingsRoute {
icon: IconName::GalleryVerticalEnd,
title: "Spaces",
detail: "Space identity, accent color, and active context.",
route: "ely://settings/spaces",
},
SettingsRoute {
icon: IconName::CircleUser,
title: "Profiles",
@@ -0,0 +1,251 @@
use ely_browser_core::BrowserSnapshot;
use ely_design_system::colors;
use ely_domain::{ArchivePolicy, Space, SpaceId};
use gpui::{
AnyElement, Context, InteractiveElement, IntoElement, ParentElement, SharedString, Styled, div,
px, rgb,
};
use gpui_component::{
IconName, Sizable, StyledExt,
button::{Button, ButtonVariants},
scroll::ScrollableElement,
};
use super::{ElyShell, render_canvas_surface};
impl ElyShell {
pub(super) fn render_spaces_page(
&mut self,
snapshot: &BrowserSnapshot,
cx: &mut Context<Self>,
) -> AnyElement {
render_canvas_surface(
div()
.size_full()
.p_8()
.flex()
.flex_col()
.gap_5()
.child(render_spaces_header(snapshot))
.child(render_active_space_summary(snapshot))
.child(render_spaces_list(snapshot, cx)),
)
}
}
fn render_spaces_header(snapshot: &BrowserSnapshot) -> AnyElement {
div()
.flex()
.items_end()
.justify_between()
.gap_4()
.child(
div()
.min_w_0()
.flex()
.flex_col()
.gap_2()
.child(div().text_size(px(26.0)).text_color(rgb(colors::INK)).child("Spaces"))
.child(
div()
.text_sm()
.truncate()
.text_color(rgb(colors::MUTED))
.child(format!("Profile: {}", snapshot.active_profile_name)),
),
)
.child(
div()
.flex()
.items_center()
.gap_2()
.text_xs()
.font_semibold()
.text_color(rgb(colors::MUTED))
.child(IconName::GalleryVerticalEnd)
.child(format!("{} spaces", snapshot.spaces.len())),
)
.into_any_element()
}
fn render_active_space_summary(snapshot: &BrowserSnapshot) -> AnyElement {
let Some(active_space) =
snapshot.spaces.iter().find(|space| space.id() == &snapshot.active_space_id)
else {
return div()
.rounded_md()
.border_1()
.border_color(rgb(colors::ERROR))
.px_4()
.py_3()
.text_sm()
.text_color(rgb(colors::ERROR))
.child("Active Space is unavailable.")
.into_any_element();
};
div()
.rounded_md()
.border_1()
.border_color(rgb(colors::HAIRLINE))
.bg(rgb(colors::CANVAS_SOFT))
.px_4()
.py_3()
.flex()
.items_center()
.justify_between()
.gap_4()
.child(
div().min_w_0().flex().items_center().gap_3().child(space_avatar(active_space)).child(
div()
.min_w_0()
.flex()
.flex_col()
.gap_1()
.child(
div()
.text_sm()
.font_semibold()
.text_color(rgb(colors::INK))
.child(active_space.name().to_string()),
)
.child(div().text_xs().truncate().text_color(rgb(colors::MUTED)).child(
format!(
"{} - {}",
accent_label(active_space.accent_hex()),
archive_policy_label(active_space.archive_policy())
),
)),
),
)
.child(div().text_xs().font_semibold().text_color(rgb(colors::SUCCESS)).child("Active"))
.into_any_element()
}
fn render_spaces_list(snapshot: &BrowserSnapshot, cx: &mut Context<ElyShell>) -> AnyElement {
div()
.flex_1()
.min_h_0()
.flex()
.flex_col()
.overflow_y_scrollbar()
.border_t_1()
.border_color(rgb(colors::HAIRLINE))
.children(snapshot.spaces.iter().enumerate().map(|(index, space)| {
render_space_row(index, space, space.id() == &snapshot.active_space_id, cx)
}))
.into_any_element()
}
fn render_space_row(
index: usize,
space: &Space,
active: bool,
cx: &mut Context<ElyShell>,
) -> AnyElement {
let space_id = space.id().clone();
div()
.id(SharedString::from(format!("settings-space-{}", space.id().as_str())))
.py_3()
.border_b_1()
.border_color(rgb(colors::HAIRLINE))
.flex()
.items_center()
.justify_between()
.gap_4()
.child(
div().min_w_0().flex().items_center().gap_3().child(space_avatar(space)).child(
div()
.min_w_0()
.flex()
.flex_col()
.gap_1()
.child(
div()
.text_sm()
.font_semibold()
.truncate()
.text_color(rgb(colors::INK))
.child(space.name().to_string()),
)
.child(
div()
.text_xs()
.truncate()
.text_color(rgb(colors::MUTED))
.child(space_detail_label(space)),
),
),
)
.child(render_space_action(index, space_id, active, cx))
.into_any_element()
}
fn render_space_action(
index: usize,
space_id: SpaceId,
active: bool,
cx: &mut Context<ElyShell>,
) -> AnyElement {
if active {
return div()
.text_xs()
.font_semibold()
.text_color(rgb(colors::SUCCESS))
.child("Active")
.into_any_element();
}
Button::new(("switch-space", index))
.small()
.primary()
.icon(IconName::Check)
.label("Switch")
.tooltip("Switch Space")
.on_click(cx.listener(move |shell, _, window, cx| {
shell.select_space(&space_id, window, cx);
}))
.into_any_element()
}
fn space_avatar(space: &Space) -> AnyElement {
div()
.w(px(28.0))
.h(px(28.0))
.rounded_md()
.border_1()
.border_color(rgb(colors::HAIRLINE_STRONG))
.bg(rgb(space.accent_hex()))
.flex()
.items_center()
.justify_center()
.text_xs()
.font_semibold()
.text_color(rgb(colors::CANVAS))
.child(space.icon().to_string())
.into_any_element()
}
fn space_detail_label(space: &Space) -> String {
format!(
"{} - {}",
accent_label(space.accent_hex()),
archive_policy_label(space.archive_policy())
)
}
fn accent_label(accent_hex: u32) -> String {
format!("#{accent_hex:06X}")
}
fn archive_policy_label(policy: &ArchivePolicy) -> &'static str {
match policy {
ArchivePolicy::Manual => "Manual archive",
ArchivePolicy::IdleDays(0) => "Archive today",
ArchivePolicy::IdleDays(1) => "Archive after 1 day",
ArchivePolicy::IdleDays(7) => "Archive after 7 days",
ArchivePolicy::IdleDays(30) => "Archive after 30 days",
ArchivePolicy::IdleDays(_) => "Custom archive policy",
}
}