diff --git a/crates/ely_app/src/shell/chrome/mod.rs b/crates/ely_app/src/shell/chrome/mod.rs index 8ccfe3b..9d942df 100644 --- a/crates/ely_app/src/shell/chrome/mod.rs +++ b/crates/ely_app/src/shell/chrome/mod.rs @@ -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; diff --git a/crates/ely_app/src/shell/chrome/settings_layout.rs b/crates/ely_app/src/shell/chrome/settings_layout.rs new file mode 100644 index 0000000..a5f415d --- /dev/null +++ b/crates/ely_app/src/shell/chrome/settings_layout.rs @@ -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, +) -> 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, +) -> 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, +) -> 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, +) -> 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, +) -> 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) -> 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, +) -> 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; diff --git a/crates/ely_app/src/shell/internal_pages/settings.rs b/crates/ely_app/src/shell/internal_pages/settings.rs index 93d139d..5530e60 100644 --- a/crates/ely_app/src/shell/internal_pages/settings.rs +++ b/crates/ely_app/src/shell/internal_pages/settings.rs @@ -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, ) -> 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) -> 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, -) -> 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() -}