From 16e8cf2e07639a941fd1be9c31e8caec33adfbfe 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 15:44:05 -0400 Subject: [PATCH] Redesign new tab page with hero heading and favorites grid Replace plain list layout with centered hero section ("Where focus finds flow."), pill-shaped search placeholder, quick action pills, favorites card grid with initial avatars, and clean tab list with glass-style row backgrounds. --- crates/ely_app/src/shell/internal_pages.rs | 10 +- .../src/shell/internal_pages/new_tab.rs | 389 ++++++++++-------- 2 files changed, 228 insertions(+), 171 deletions(-) diff --git a/crates/ely_app/src/shell/internal_pages.rs b/crates/ely_app/src/shell/internal_pages.rs index 4e074bd..f74494b 100644 --- a/crates/ely_app/src/shell/internal_pages.rs +++ b/crates/ely_app/src/shell/internal_pages.rs @@ -36,7 +36,7 @@ mod task_manager; mod updates; use ely_browser_core::BrowserSnapshot; -use ely_design_system::{colors, spacing}; +use ely_design_system::colors; use ely_domain::{ArchivedTab, BrowserTab, TabState}; use gpui::{ AnyElement, Context, InteractiveElement, IntoElement, ParentElement, SharedString, @@ -251,15 +251,11 @@ fn render_canvas_surface(content: impl IntoElement) -> AnyElement { div() .flex_1() .h_full() - .p(px(spacing::LG)) - .bg(rgb(colors::CANVAS_SOFT)) + .overflow_hidden() .child( div() .size_full() - .rounded_lg() - .border_1() - .border_color(rgb(colors::HAIRLINE)) - .bg(rgb(colors::SURFACE_CARD)) + .overflow_y_scrollbar() .child(content), ) .into_any_element() diff --git a/crates/ely_app/src/shell/internal_pages/new_tab.rs b/crates/ely_app/src/shell/internal_pages/new_tab.rs index b3d89f5..e52c9fc 100644 --- a/crates/ely_app/src/shell/internal_pages/new_tab.rs +++ b/crates/ely_app/src/shell/internal_pages/new_tab.rs @@ -3,28 +3,24 @@ use ely_design_system::colors; use ely_domain::{BrowserTab, TabId}; use gpui::{ AnyElement, Context, InteractiveElement, IntoElement, ParentElement, SharedString, - StatefulInteractiveElement, Styled, div, px, rgb, -}; -use gpui_component::{ - IconName, Sizable, StyledExt, - button::{Button, ButtonVariants}, - scroll::ScrollableElement, + StatefulInteractiveElement, Styled, div, px, rgb, rgba, }; +use gpui_component::{IconName, StyledExt, scroll::ScrollableElement}; -use super::{ElyShell, render_canvas_surface}; +use super::ElyShell; -struct NewTabAction { +struct QuickAction { label: &'static str, route: &'static str, icon: IconName, } -const NEW_TAB_ACTIONS: &[NewTabAction] = &[ - NewTabAction { label: "History", route: "ely://history", icon: IconName::Undo2 }, - NewTabAction { label: "Bookmarks", route: "ely://bookmarks", icon: IconName::BookOpen }, - NewTabAction { label: "Downloads", route: "ely://downloads", icon: IconName::Folder }, - NewTabAction { label: "Plugins", route: "ely://plugins", icon: IconName::Asterisk }, - NewTabAction { label: "Settings", route: "ely://settings", icon: IconName::Settings2 }, +const QUICK_ACTIONS: &[QuickAction] = &[ + QuickAction { label: "History", route: "ely://history", icon: IconName::Undo2 }, + QuickAction { label: "Bookmarks", route: "ely://bookmarks", icon: IconName::BookOpen }, + QuickAction { label: "Downloads", route: "ely://downloads", icon: IconName::Folder }, + QuickAction { label: "Plugins", route: "ely://plugins", icon: IconName::Asterisk }, + QuickAction { label: "Settings", route: "ely://settings", icon: IconName::Settings2 }, ]; impl ElyShell { @@ -33,155 +29,228 @@ impl ElyShell { snapshot: &BrowserSnapshot, cx: &mut Context, ) -> AnyElement { - render_canvas_surface( - div() - .size_full() - .p_8() - .flex() - .flex_col() - .gap_6() - .child(render_new_tab_header(snapshot)) - .child(render_new_tab_actions(cx)) - .child(render_new_tab_metrics(snapshot)) - .child(render_new_tab_tab_list(snapshot, cx)), - ) + div() + .flex_1() + .h_full() + .overflow_y_scrollbar() + .child( + div() + .size_full() + .p(px(48.0)) + .flex() + .flex_col() + .gap(px(24.0)) + .child(render_hero_section()) + .child(render_quick_actions(cx)) + .child(render_favorites_grid(snapshot, cx)) + .child(render_tabs_section(snapshot, cx)), + ) + .into_any_element() } } -fn render_new_tab_header(snapshot: &BrowserSnapshot) -> AnyElement { +fn render_hero_section() -> AnyElement { div() .flex() - .items_start() - .justify_between() - .gap_5() + .flex_col() + .items_center() + .gap(px(20.0)) + .pt(px(24.0)) + .pb(px(12.0)) .child( div() - .min_w_0() - .flex() - .flex_col() - .gap_2() - .child(div().text_size(px(30.0)).text_color(rgb(colors::INK)).child("New Tab")) - .child( - div() - .text_sm() - .text_color(rgb(colors::MUTED)) - .child(format!("{} space", snapshot.active_space_name)), - ), + .text_size(px(48.0)) + .font_weight(gpui::FontWeight(400.0)) + .text_color(rgb(colors::INK)) + .child("Where focus finds flow."), ) .child( div() + .w(px(420.0)) + .h(px(44.0)) + .rounded(px(999.0)) + .bg(rgba(colors::GLASS_2)) + .px(px(16.0)) .flex() .items_center() - .gap_2() - .rounded_md() - .border_1() - .border_color(rgb(colors::HAIRLINE)) - .px_3() - .py_2() - .text_xs() - .text_color(rgb(colors::MUTED)) - .child(IconName::CircleUser) - .child(snapshot.active_profile_name.clone()), + .gap(px(10.0)) + .child(div().text_color(rgb(colors::INK_4)).child(IconName::Search)) + .child( + div() + .text_size(px(14.0)) + .text_color(rgb(colors::INK_4)) + .child("Search the web or ELY"), + ), ) .into_any_element() } -fn render_new_tab_actions(cx: &mut Context) -> AnyElement { +fn render_quick_actions(cx: &mut Context) -> AnyElement { div() - .grid() - .grid_cols(5) - .gap_3() - .children( - NEW_TAB_ACTIONS - .iter() - .enumerate() - .map(|(index, action)| render_new_tab_action(index, action, cx)), - ) - .into_any_element() -} - -fn render_new_tab_action( - index: usize, - action: &'static NewTabAction, - cx: &mut Context, -) -> AnyElement { - Button::new(("new-tab-action", index)) - .ghost() - .small() - .icon(action.icon.clone()) - .label(action.label) - .tooltip(action.label) - .on_click(cx.listener(move |shell, _, window, cx| { - shell.open_internal_tab(action.route, window, cx); + .flex() + .items_center() + .justify_center() + .gap(px(8.0)) + .children(QUICK_ACTIONS.iter().enumerate().map(|(index, action)| { + let route = action.route; + div() + .id(SharedString::from(format!("quick-action-{index}"))) + .rounded(px(999.0)) + .bg(rgba(colors::GLASS_2)) + .px(px(12.0)) + .py(px(6.0)) + .flex() + .items_center() + .gap(px(6.0)) + .cursor_pointer() + .hover(|style| style.bg(rgba(colors::GLASS_3))) + .active(|style| style.opacity(0.82)) + .on_click(cx.listener(move |shell, _, window, cx| { + shell.open_internal_tab(route, window, cx); + })) + .child( + div() + .text_color(rgb(colors::INK_3)) + .text_size(px(12.0)) + .child(action.icon.clone()), + ) + .child( + div() + .text_size(px(12.0)) + .font_weight(gpui::FontWeight(500.0)) + .text_color(rgb(colors::INK_2)) + .child(action.label), + ) })) .into_any_element() } -fn render_new_tab_metrics(snapshot: &BrowserSnapshot) -> AnyElement { - div() - .border_t_1() - .border_b_1() - .border_color(rgb(colors::HAIRLINE)) - .py_3() - .grid() - .grid_cols(4) - .gap_4() - .child(new_tab_metric("Open Tabs", snapshot.tabs.len())) - .child(new_tab_metric("Favorites", snapshot.favorites.len())) - .child(new_tab_metric("History", snapshot.active_profile_history_entry_count)) - .child(new_tab_metric("Downloads", snapshot.download_entries.len())) - .into_any_element() -} +fn render_favorites_grid(snapshot: &BrowserSnapshot, cx: &mut Context) -> AnyElement { + if snapshot.favorites.is_empty() { + return div().into_any_element(); + } -fn new_tab_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)) + .gap(px(12.0)) .child( - div().text_sm().font_semibold().text_color(rgb(colors::INK)).child(value.to_string()), + div() + .text_size(px(10.5)) + .font_weight(gpui::FontWeight(500.0)) + .text_color(rgb(colors::INK_4)) + .child("FAVORITES"), ) - .into_any_element() -} - -fn render_new_tab_tab_list(snapshot: &BrowserSnapshot, cx: &mut Context) -> AnyElement { - div() - .flex_1() - .min_h_0() - .flex() - .flex_col() - .gap_3() .child( div() .flex() - .items_center() - .justify_between() - .gap_3() - .child(div().text_sm().font_semibold().text_color(rgb(colors::INK)).child("Tabs")) - .child( - div() - .text_xs() - .text_color(rgb(colors::MUTED)) - .child(format!("{} visible", snapshot.tabs.len())), - ), - ) - .child( - div() - .flex_1() - .min_h_0() - .overflow_y_scrollbar() - .border_t_1() - .border_color(rgb(colors::HAIRLINE)) - .children(snapshot.tabs.iter().enumerate().map(|(index, tab)| { - render_new_tab_tab_row(index, tab, &snapshot.active_tab_id, cx) + .flex_wrap() + .gap(px(10.0)) + .children(snapshot.favorites.iter().enumerate().map(|(index, tab)| { + render_favorite_card(index, tab, cx) })), ) .into_any_element() } -fn render_new_tab_tab_row( +fn render_favorite_card( + index: usize, + tab: &BrowserTab, + cx: &mut Context, +) -> AnyElement { + let tab_id = tab.id().clone(); + let initial = tab + .title() + .chars() + .next() + .unwrap_or('?') + .to_uppercase() + .to_string(); + + div() + .id(SharedString::from(format!("fav-card-{index}"))) + .w(px(96.0)) + .h(px(96.0)) + .rounded(px(14.0)) + .bg(rgba(colors::GLASS_2)) + .flex() + .flex_col() + .items_center() + .justify_center() + .gap(px(8.0)) + .cursor_pointer() + .hover(|style| style.bg(rgba(colors::GLASS_3))) + .active(|style| style.opacity(0.82)) + .on_click(cx.listener(move |shell, _, window, cx| { + shell.select_tab(&tab_id, window, cx); + })) + .child( + div() + .size(px(36.0)) + .rounded(px(10.0)) + .bg(rgb(colors::ACCENT)) + .flex() + .items_center() + .justify_center() + .child( + div() + .text_size(px(16.0)) + .font_semibold() + .text_color(rgb(colors::SURFACE_CARD)) + .child(initial), + ), + ) + .child( + div() + .text_size(px(11.0)) + .text_color(rgb(colors::INK_2)) + .max_w(px(80.0)) + .truncate() + .child(tab.title().to_string()), + ) + .into_any_element() +} + +fn render_tabs_section(snapshot: &BrowserSnapshot, cx: &mut Context) -> AnyElement { + if snapshot.tabs.is_empty() { + return div().into_any_element(); + } + + div() + .flex() + .flex_col() + .gap(px(12.0)) + .child( + div() + .flex() + .items_center() + .justify_between() + .child( + div() + .text_size(px(10.5)) + .font_weight(gpui::FontWeight(500.0)) + .text_color(rgb(colors::INK_4)) + .child("OPEN TABS"), + ) + .child( + div() + .text_size(px(11.0)) + .text_color(rgb(colors::INK_4)) + .child(format!("{}", snapshot.tabs.len())), + ), + ) + .child( + div() + .flex() + .flex_col() + .children(snapshot.tabs.iter().enumerate().map(|(index, tab)| { + render_tab_row(index, tab, &snapshot.active_tab_id, cx) + })), + ) + .into_any_element() +} + +fn render_tab_row( index: usize, tab: &BrowserTab, active_tab_id: &TabId, @@ -189,58 +258,50 @@ fn render_new_tab_tab_row( ) -> AnyElement { let tab_id = tab.id().clone(); let active = tab.id() == active_tab_id; - let icon = if active { IconName::CircleCheck } else { IconName::Globe }; - let icon_color = if active { colors::PRIMARY } else { colors::MUTED_SOFT }; + let bg = if active { colors::GLASS_3 } else { 0x00000000 }; div() - .id(SharedString::from(format!("new-tab-row-{index}"))) - .py_3() - .border_b_1() - .border_color(rgb(colors::HAIRLINE)) + .id(SharedString::from(format!("tab-row-{index}"))) + .rounded(px(8.0)) + .px(px(10.0)) + .py(px(8.0)) .flex() .items_center() - .justify_between() - .gap_4() + .gap(px(10.0)) .cursor_pointer() - .hover(|style| style.bg(rgb(colors::CANVAS_SOFT))) + .bg(rgba(bg)) + .hover(|style| style.bg(rgba(colors::GLASS_3))) .active(|style| style.opacity(0.82)) .on_click(cx.listener(move |shell, _, window, cx| { shell.select_tab(&tab_id, window, cx); })) + .child( + div() + .text_color(if active { rgb(colors::ACCENT) } else { rgb(colors::INK_4) }) + .child(IconName::Globe), + ) .child( div() .min_w_0() + .flex_1() .flex() - .items_center() - .gap_3() - .child(div().text_color(rgb(icon_color)).child(icon)) + .flex_col() + .gap_1() .child( div() - .min_w_0() - .flex() - .flex_col() - .gap_1() - .child( - div() - .text_sm() - .font_semibold() - .truncate() - .text_color(rgb(colors::INK)) - .child(tab.title().to_string()), - ) - .child( - div() - .text_xs() - .truncate() - .text_color(rgb(colors::MUTED)) - .child(tab.display_url()), - ), + .text_size(px(13.0)) + .font_weight(gpui::FontWeight(500.0)) + .text_color(rgb(colors::INK)) + .truncate() + .child(tab.title().to_string()), + ) + .child( + div() + .text_size(px(11.0)) + .text_color(rgb(colors::INK_4)) + .truncate() + .child(tab.display_url()), ), ) - .child(div().text_xs().text_color(rgb(colors::MUTED)).child(if active { - "Active" - } else { - "Open" - })) .into_any_element() }