Restructure settings landing as design's nav + content split

Settings now opens with the design's two-column layout: a 232 wide nav
column on the left with grouped sections (General, Account, Power,
About) and a content column on the right that leads with a serif
headline, snapshot metric cards, and a "Jump in" list of every settings
route. Each nav entry and jump row deep-links to the existing internal
page so the destinations remain real.

The previous settings.rs is now a thin shim that calls into the chrome
module — keeps the module under 500 lines and lets future
sub-page redesigns share the same shell layout.
This commit is contained in:
2026-05-09 18:14:59 -04:00
parent 7f1870295d
commit cc43c1ed01
3 changed files with 412 additions and 257 deletions
+2
View File
@@ -1,5 +1,6 @@
pub(crate) mod command_overlay;
pub(crate) mod home;
pub(crate) mod settings_layout;
pub(crate) mod sidebar;
pub(crate) mod sidebar_header;
pub(crate) mod topbar;
@@ -7,6 +8,7 @@ pub(crate) mod wallpaper;
pub(crate) use command_overlay::render_command_overlay;
pub(crate) use home::render_home_page;
pub(crate) use settings_layout::render_settings_landing;
pub(crate) use sidebar::{PANEL_BG, panel_shadow};
pub(crate) use sidebar_header::render_sidebar_header;
pub(crate) use topbar::render_topbar;
@@ -0,0 +1,406 @@
use ely_browser_core::BrowserSnapshot;
use ely_design_system::colors;
use gpui::{
AnyElement, Context, FontWeight, InteractiveElement, IntoElement, ParentElement, SharedString,
StatefulInteractiveElement, Styled, div, px, rgb, rgba,
};
use gpui_component::{IconName, scroll::ScrollableElement};
use crate::shell::ElyShell;
struct NavGroup {
label: &'static str,
items: &'static [NavItem],
}
struct NavItem {
icon: IconName,
label: &'static str,
route: &'static str,
}
const NAV_GROUPS: &[NavGroup] = &[
NavGroup {
label: "GENERAL",
items: &[
NavItem {
icon: IconName::Palette,
label: "Appearance",
route: "ely://settings/appearance",
},
NavItem {
icon: IconName::PanelLeft,
label: "Sidebar & Tabs",
route: "ely://settings/sidebar-tabs",
},
NavItem {
icon: IconName::GalleryVerticalEnd,
label: "Spaces",
route: "ely://settings/spaces",
},
NavItem {
icon: IconName::Search,
label: "Search",
route: "ely://settings/search",
},
NavItem {
icon: IconName::SquareTerminal,
label: "Shortcuts",
route: "ely://settings/shortcuts",
},
],
},
NavGroup {
label: "ACCOUNT",
items: &[
NavItem {
icon: IconName::ChartPie,
label: "Sync",
route: "ely://settings/sync",
},
NavItem {
icon: IconName::Eye,
label: "Privacy & Security",
route: "ely://settings/privacy-security",
},
NavItem {
icon: IconName::CircleUser,
label: "Profiles",
route: "ely://settings/profiles",
},
NavItem {
icon: IconName::Folder,
label: "Downloads",
route: "ely://settings/downloads",
},
NavItem {
icon: IconName::Globe,
label: "Site Permissions",
route: "ely://settings/site-permissions",
},
],
},
NavGroup {
label: "POWER",
items: &[
NavItem {
icon: IconName::Asterisk,
label: "Plugins",
route: "ely://settings/plugins",
},
NavItem {
icon: IconName::LoaderCircle,
label: "Updates",
route: "ely://settings/updates",
},
NavItem {
icon: IconName::Inspector,
label: "Advanced",
route: "ely://settings/advanced",
},
],
},
NavGroup {
label: "ABOUT",
items: &[NavItem {
icon: IconName::Info,
label: "About",
route: "ely://about",
}],
},
];
pub(crate) fn render_settings_landing(
snapshot: &BrowserSnapshot,
active_route: &str,
cx: &mut Context<ElyShell>,
) -> AnyElement {
div()
.flex_1()
.h_full()
.flex()
.child(render_nav_column(snapshot, active_route, cx))
.child(render_content_column(snapshot, cx))
.into_any_element()
}
fn render_nav_column(
snapshot: &BrowserSnapshot,
active_route: &str,
cx: &mut Context<ElyShell>,
) -> AnyElement {
div()
.w(px(232.0))
.h_full()
.flex_shrink_0()
.border_r_1()
.border_color(rgba(colors::DIVIDER))
.px(px(10.0))
.pt(px(16.0))
.pb(px(12.0))
.flex()
.flex_col()
.gap(px(2.0))
.overflow_y_scrollbar()
.child(render_nav_brand(snapshot))
.children(NAV_GROUPS.iter().enumerate().map(|(group_index, group)| {
render_nav_group(group_index, group, active_route, cx)
}))
.into_any_element()
}
fn render_nav_brand(snapshot: &BrowserSnapshot) -> AnyElement {
div()
.px(px(12.0))
.pb(px(12.0))
.flex()
.flex_col()
.gap_1()
.child(
div()
.text_size(px(18.0))
.font_weight(FontWeight(500.0))
.text_color(rgb(colors::INK))
.child("Settings"),
)
.child(
div()
.text_size(px(11.5))
.text_color(rgb(colors::INK_4))
.child(format!(
"ELY 0.42 · Profile: {}",
snapshot.active_profile_name
)),
)
.into_any_element()
}
fn render_nav_group(
group_index: usize,
group: &NavGroup,
active_route: &str,
cx: &mut Context<ElyShell>,
) -> AnyElement {
div()
.flex()
.flex_col()
.gap(px(1.0))
.child(render_nav_label(group.label))
.children(group.items.iter().enumerate().map(|(item_index, item)| {
render_nav_item(group_index, item_index, item, active_route, cx)
}))
.into_any_element()
}
fn render_nav_label(label: &'static str) -> AnyElement {
div()
.pt(px(8.0))
.pb(px(4.0))
.px(px(12.0))
.text_size(px(10.0))
.font_weight(FontWeight(500.0))
.text_color(rgb(colors::INK_4))
.child(label)
.into_any_element()
}
fn render_nav_item(
group_index: usize,
item_index: usize,
item: &NavItem,
active_route: &str,
cx: &mut Context<ElyShell>,
) -> AnyElement {
let active = item.route == active_route;
let bg = if active { ACTIVE_BG } else { 0x00000000 };
let text_color = if active { colors::INK } else { colors::INK_2 };
let route = item.route;
let icon = item.icon.clone();
div()
.id(SharedString::from(format!(
"settings-nav-{group_index}-{item_index}"
)))
.flex()
.items_center()
.gap(px(10.0))
.px(px(12.0))
.py(px(7.0))
.rounded(px(8.0))
.bg(rgba(bg))
.text_size(px(13.0))
.text_color(rgb(text_color))
.cursor_pointer()
.hover(|style| style.bg(rgba(HOVER_BG)))
.active(|style| style.opacity(0.85))
.on_click(cx.listener(move |shell, _, window, cx| {
shell.open_internal_tab(route, window, cx);
}))
.child(
div()
.text_color(rgb(colors::INK_3))
.child(icon),
)
.child(div().flex_1().truncate().child(item.label))
.into_any_element()
}
fn render_content_column(
snapshot: &BrowserSnapshot,
cx: &mut Context<ElyShell>,
) -> AnyElement {
div()
.flex_1()
.h_full()
.overflow_y_scrollbar()
.pt(px(28.0))
.px(px(40.0))
.pb(px(32.0))
.flex()
.flex_col()
.gap(px(24.0))
.child(render_content_header())
.child(render_summary_metrics(snapshot))
.child(render_quick_links(cx))
.into_any_element()
}
fn render_content_header() -> AnyElement {
div()
.flex()
.flex_col()
.gap(px(8.0))
.max_w(px(620.0))
.child(
div()
.text_size(px(11.0))
.text_color(rgb(colors::INK_4))
.child("GENERAL"),
)
.child(
div()
.text_size(px(34.0))
.font_weight(FontWeight(400.0))
.text_color(rgb(colors::INK))
.child("Settings"),
)
.child(
div()
.text_size(px(13.0))
.text_color(rgb(colors::INK_3))
.child(
"Tune the atmosphere of ELY — choose a wallpaper, configure sync, install \
plugins. Each section opens its own page.",
),
)
.into_any_element()
}
fn render_summary_metrics(snapshot: &BrowserSnapshot) -> AnyElement {
div()
.flex()
.gap(px(10.0))
.child(metric_card("Profiles", snapshot.profiles.len()))
.child(metric_card("Spaces", snapshot.spaces.len()))
.child(metric_card("Plugins", snapshot.installed_plugins.len()))
.child(metric_card("Open tabs", snapshot.tabs.len()))
.into_any_element()
}
fn metric_card(label: &'static str, value: usize) -> AnyElement {
div()
.flex_1()
.px(px(14.0))
.py(px(12.0))
.rounded(px(12.0))
.bg(rgba(METRIC_BG))
.border_1()
.border_color(rgba(colors::STROKE_2))
.flex()
.flex_col()
.gap_1()
.child(
div()
.text_size(px(10.5))
.text_color(rgb(colors::INK_4))
.child(label),
)
.child(
div()
.text_size(px(18.0))
.font_weight(FontWeight(500.0))
.text_color(rgb(colors::INK))
.child(value.to_string()),
)
.into_any_element()
}
fn render_quick_links(cx: &mut Context<ElyShell>) -> AnyElement {
div()
.flex()
.flex_col()
.gap(px(8.0))
.child(
div()
.text_size(px(11.0))
.text_color(rgb(colors::INK_4))
.child("JUMP IN"),
)
.children(NAV_GROUPS.iter().flat_map(|group| group.items).enumerate().map(
|(index, item)| render_quick_link(index, item, cx),
))
.into_any_element()
}
fn render_quick_link(
index: usize,
item: &NavItem,
cx: &mut Context<ElyShell>,
) -> AnyElement {
let route = item.route;
let icon = item.icon.clone();
div()
.id(SharedString::from(format!("settings-jump-{index}")))
.flex()
.items_center()
.gap(px(12.0))
.py(px(10.0))
.border_b_1()
.border_color(rgba(colors::DIVIDER))
.cursor_pointer()
.hover(|style| style.opacity(0.92))
.active(|style| style.opacity(0.78))
.on_click(cx.listener(move |shell, _, window, cx| {
shell.open_internal_tab(route, window, cx);
}))
.child(
div()
.size(px(28.0))
.rounded(px(7.0))
.bg(rgba(METRIC_BG))
.flex()
.items_center()
.justify_center()
.text_color(rgb(colors::INK_3))
.child(icon),
)
.child(
div()
.flex_1()
.text_size(px(13.0))
.font_weight(FontWeight(500.0))
.text_color(rgb(colors::INK))
.child(item.label),
)
.child(
div()
.text_color(rgb(colors::INK_4))
.child(IconName::ChevronRight),
)
.into_any_element()
}
const ACTIVE_BG: u32 = 0xffffffd9;
const HOVER_BG: u32 = 0xffffff8c;
const METRIC_BG: u32 = 0xffffffc7;
@@ -1,114 +1,8 @@
use ely_browser_core::BrowserSnapshot;
use ely_design_system::colors;
use gpui::{AnyElement, Context, IntoElement, ParentElement, Styled, div, px, rgb};
use gpui_component::{
IconName, Sizable, StyledExt,
button::{Button, ButtonVariants},
scroll::ScrollableElement,
};
use gpui::{AnyElement, Context};
use super::{ElyShell, render_canvas_surface};
#[derive(Clone)]
struct SettingsRoute {
icon: IconName,
title: &'static str,
detail: &'static str,
route: &'static str,
}
const SETTINGS_ROUTES: &[SettingsRoute] = &[
SettingsRoute {
icon: IconName::Settings2,
title: "General",
detail: "New Tab destination and browser startup defaults.",
route: "ely://settings/general",
},
SettingsRoute {
icon: IconName::Palette,
title: "Appearance",
detail: "Theme tokens, Space accent, and Profile color.",
route: "ely://settings/appearance",
},
SettingsRoute {
icon: IconName::LayoutDashboard,
title: "Sidebar & Tabs",
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::Search,
title: "Search",
detail: "Default search engine for Command Bar queries.",
route: "ely://settings/search",
},
SettingsRoute {
icon: IconName::Eye,
title: "Privacy & Security",
detail: "History recording and profile-scoped privacy controls.",
route: "ely://settings/privacy-security",
},
SettingsRoute {
icon: IconName::Folder,
title: "Downloads",
detail: "Profile download location and save behavior.",
route: "ely://settings/downloads",
},
SettingsRoute {
icon: IconName::Globe,
title: "Site Permissions",
detail: "Profile-scoped site permissions and local audit state.",
route: "ely://settings/site-permissions",
},
SettingsRoute {
icon: IconName::CircleUser,
title: "Profiles",
detail: "Profile identity, color, and download policy.",
route: "ely://settings/profiles",
},
SettingsRoute {
icon: IconName::SquareTerminal,
title: "Shortcuts",
detail: "Registered key bindings and conflict state.",
route: "ely://settings/shortcuts",
},
SettingsRoute {
icon: IconName::Globe,
title: "Sync",
detail: "Local sync state and object scope.",
route: "ely://settings/sync",
},
SettingsRoute {
icon: IconName::LoaderCircle,
title: "Updates",
detail: "Build identity and Elydora release manifest contract.",
route: "ely://settings/updates",
},
SettingsRoute {
icon: IconName::Inspector,
title: "Advanced",
detail: "Local runtime policies and audit counters.",
route: "ely://settings/advanced",
},
SettingsRoute {
icon: IconName::Asterisk,
title: "Plugins",
detail: "Installed plugin control and audit trail.",
route: "ely://settings/plugins",
},
SettingsRoute {
icon: IconName::Info,
title: "About",
detail: "Build, runtime, license, and product information.",
route: "ely://about",
},
];
use super::ElyShell;
use crate::shell::chrome::render_settings_landing;
impl ElyShell {
pub(super) fn render_settings_page(
@@ -116,153 +10,6 @@ impl ElyShell {
snapshot: &BrowserSnapshot,
cx: &mut Context<Self>,
) -> AnyElement {
render_canvas_surface(
div()
.size_full()
.p_8()
.flex()
.flex_col()
.gap_5()
.child(render_settings_header(snapshot))
.child(render_settings_summary(snapshot))
.child(render_settings_routes(cx)),
)
render_settings_landing(snapshot, "ely://settings", cx)
}
}
fn render_settings_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("Settings"))
.child(
div()
.text_sm()
.truncate()
.text_color(rgb(colors::MUTED))
.child(format!("Profile: {}", snapshot.active_profile_name)),
),
)
.child(
div()
.text_xs()
.font_semibold()
.text_color(rgb(colors::MUTED))
.child(format!("{} routes", SETTINGS_ROUTES.len())),
)
.into_any_element()
}
fn render_settings_summary(snapshot: &BrowserSnapshot) -> AnyElement {
div()
.border_t_1()
.border_b_1()
.border_color(rgb(colors::HAIRLINE))
.py_3()
.flex()
.items_center()
.justify_between()
.gap_4()
.children([
settings_metric("Profiles", snapshot.profiles.len()),
settings_metric("Plugins", snapshot.installed_plugins.len()),
settings_metric("Sync objects", snapshot.sync_status.objects().len()),
settings_metric("Open tabs", snapshot.tabs.len()),
])
.into_any_element()
}
fn settings_metric(label: &'static str, value: usize) -> AnyElement {
div()
.min_w_0()
.flex()
.flex_col()
.gap_1()
.child(div().text_xs().text_color(rgb(colors::MUTED)).child(label))
.child(
div().text_sm().font_semibold().text_color(rgb(colors::INK)).child(value.to_string()),
)
.into_any_element()
}
fn render_settings_routes(cx: &mut Context<ElyShell>) -> AnyElement {
div()
.flex_1()
.min_h_0()
.flex()
.flex_col()
.overflow_y_scrollbar()
.children(
SETTINGS_ROUTES
.iter()
.enumerate()
.map(|(index, route)| render_settings_route(index, route, cx)),
)
.into_any_element()
}
fn render_settings_route(
index: usize,
settings_route: &SettingsRoute,
cx: &mut Context<ElyShell>,
) -> AnyElement {
let route = settings_route.route;
div()
.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(div().text_color(rgb(colors::MUTED)).child(settings_route.icon.clone()))
.child(
div()
.min_w_0()
.flex()
.flex_col()
.gap_1()
.child(
div()
.text_sm()
.font_semibold()
.truncate()
.text_color(rgb(colors::INK))
.child(settings_route.title),
)
.child(
div()
.text_xs()
.truncate()
.text_color(rgb(colors::MUTED))
.child(settings_route.detail),
),
),
)
.child(
Button::new(("open-settings-route", index))
.ghost()
.xsmall()
.label("Open")
.tooltip(settings_route.title)
.on_click(cx.listener(move |shell, _, window, cx| {
shell.open_internal_tab(route, window, cx);
})),
)
.into_any_element()
}