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.
This commit is contained in:
@@ -36,7 +36,7 @@ mod task_manager;
|
|||||||
mod updates;
|
mod updates;
|
||||||
|
|
||||||
use ely_browser_core::BrowserSnapshot;
|
use ely_browser_core::BrowserSnapshot;
|
||||||
use ely_design_system::{colors, spacing};
|
use ely_design_system::colors;
|
||||||
use ely_domain::{ArchivedTab, BrowserTab, TabState};
|
use ely_domain::{ArchivedTab, BrowserTab, TabState};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, Context, InteractiveElement, IntoElement, ParentElement, SharedString,
|
AnyElement, Context, InteractiveElement, IntoElement, ParentElement, SharedString,
|
||||||
@@ -251,15 +251,11 @@ fn render_canvas_surface(content: impl IntoElement) -> AnyElement {
|
|||||||
div()
|
div()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.h_full()
|
.h_full()
|
||||||
.p(px(spacing::LG))
|
.overflow_hidden()
|
||||||
.bg(rgb(colors::CANVAS_SOFT))
|
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.size_full()
|
.size_full()
|
||||||
.rounded_lg()
|
.overflow_y_scrollbar()
|
||||||
.border_1()
|
|
||||||
.border_color(rgb(colors::HAIRLINE))
|
|
||||||
.bg(rgb(colors::SURFACE_CARD))
|
|
||||||
.child(content),
|
.child(content),
|
||||||
)
|
)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
|
|||||||
@@ -3,28 +3,24 @@ use ely_design_system::colors;
|
|||||||
use ely_domain::{BrowserTab, TabId};
|
use ely_domain::{BrowserTab, TabId};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, Context, InteractiveElement, IntoElement, ParentElement, SharedString,
|
AnyElement, Context, InteractiveElement, IntoElement, ParentElement, SharedString,
|
||||||
StatefulInteractiveElement, Styled, div, px, rgb,
|
StatefulInteractiveElement, Styled, div, px, rgb, rgba,
|
||||||
};
|
|
||||||
use gpui_component::{
|
|
||||||
IconName, Sizable, StyledExt,
|
|
||||||
button::{Button, ButtonVariants},
|
|
||||||
scroll::ScrollableElement,
|
|
||||||
};
|
};
|
||||||
|
use gpui_component::{IconName, StyledExt, scroll::ScrollableElement};
|
||||||
|
|
||||||
use super::{ElyShell, render_canvas_surface};
|
use super::ElyShell;
|
||||||
|
|
||||||
struct NewTabAction {
|
struct QuickAction {
|
||||||
label: &'static str,
|
label: &'static str,
|
||||||
route: &'static str,
|
route: &'static str,
|
||||||
icon: IconName,
|
icon: IconName,
|
||||||
}
|
}
|
||||||
|
|
||||||
const NEW_TAB_ACTIONS: &[NewTabAction] = &[
|
const QUICK_ACTIONS: &[QuickAction] = &[
|
||||||
NewTabAction { label: "History", route: "ely://history", icon: IconName::Undo2 },
|
QuickAction { label: "History", route: "ely://history", icon: IconName::Undo2 },
|
||||||
NewTabAction { label: "Bookmarks", route: "ely://bookmarks", icon: IconName::BookOpen },
|
QuickAction { label: "Bookmarks", route: "ely://bookmarks", icon: IconName::BookOpen },
|
||||||
NewTabAction { label: "Downloads", route: "ely://downloads", icon: IconName::Folder },
|
QuickAction { label: "Downloads", route: "ely://downloads", icon: IconName::Folder },
|
||||||
NewTabAction { label: "Plugins", route: "ely://plugins", icon: IconName::Asterisk },
|
QuickAction { label: "Plugins", route: "ely://plugins", icon: IconName::Asterisk },
|
||||||
NewTabAction { label: "Settings", route: "ely://settings", icon: IconName::Settings2 },
|
QuickAction { label: "Settings", route: "ely://settings", icon: IconName::Settings2 },
|
||||||
];
|
];
|
||||||
|
|
||||||
impl ElyShell {
|
impl ElyShell {
|
||||||
@@ -33,155 +29,228 @@ impl ElyShell {
|
|||||||
snapshot: &BrowserSnapshot,
|
snapshot: &BrowserSnapshot,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
render_canvas_surface(
|
div()
|
||||||
div()
|
.flex_1()
|
||||||
.size_full()
|
.h_full()
|
||||||
.p_8()
|
.overflow_y_scrollbar()
|
||||||
.flex()
|
.child(
|
||||||
.flex_col()
|
div()
|
||||||
.gap_6()
|
.size_full()
|
||||||
.child(render_new_tab_header(snapshot))
|
.p(px(48.0))
|
||||||
.child(render_new_tab_actions(cx))
|
.flex()
|
||||||
.child(render_new_tab_metrics(snapshot))
|
.flex_col()
|
||||||
.child(render_new_tab_tab_list(snapshot, cx)),
|
.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()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
.items_start()
|
.flex_col()
|
||||||
.justify_between()
|
.items_center()
|
||||||
.gap_5()
|
.gap(px(20.0))
|
||||||
|
.pt(px(24.0))
|
||||||
|
.pb(px(12.0))
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.min_w_0()
|
.text_size(px(48.0))
|
||||||
.flex()
|
.font_weight(gpui::FontWeight(400.0))
|
||||||
.flex_col()
|
.text_color(rgb(colors::INK))
|
||||||
.gap_2()
|
.child("Where focus finds flow."),
|
||||||
.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)),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
|
.w(px(420.0))
|
||||||
|
.h(px(44.0))
|
||||||
|
.rounded(px(999.0))
|
||||||
|
.bg(rgba(colors::GLASS_2))
|
||||||
|
.px(px(16.0))
|
||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
.gap_2()
|
.gap(px(10.0))
|
||||||
.rounded_md()
|
.child(div().text_color(rgb(colors::INK_4)).child(IconName::Search))
|
||||||
.border_1()
|
.child(
|
||||||
.border_color(rgb(colors::HAIRLINE))
|
div()
|
||||||
.px_3()
|
.text_size(px(14.0))
|
||||||
.py_2()
|
.text_color(rgb(colors::INK_4))
|
||||||
.text_xs()
|
.child("Search the web or ELY"),
|
||||||
.text_color(rgb(colors::MUTED))
|
),
|
||||||
.child(IconName::CircleUser)
|
|
||||||
.child(snapshot.active_profile_name.clone()),
|
|
||||||
)
|
)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_new_tab_actions(cx: &mut Context<ElyShell>) -> AnyElement {
|
fn render_quick_actions(cx: &mut Context<ElyShell>) -> AnyElement {
|
||||||
div()
|
div()
|
||||||
.grid()
|
.flex()
|
||||||
.grid_cols(5)
|
.items_center()
|
||||||
.gap_3()
|
.justify_center()
|
||||||
.children(
|
.gap(px(8.0))
|
||||||
NEW_TAB_ACTIONS
|
.children(QUICK_ACTIONS.iter().enumerate().map(|(index, action)| {
|
||||||
.iter()
|
let route = action.route;
|
||||||
.enumerate()
|
div()
|
||||||
.map(|(index, action)| render_new_tab_action(index, action, cx)),
|
.id(SharedString::from(format!("quick-action-{index}")))
|
||||||
)
|
.rounded(px(999.0))
|
||||||
.into_any_element()
|
.bg(rgba(colors::GLASS_2))
|
||||||
}
|
.px(px(12.0))
|
||||||
|
.py(px(6.0))
|
||||||
fn render_new_tab_action(
|
.flex()
|
||||||
index: usize,
|
.items_center()
|
||||||
action: &'static NewTabAction,
|
.gap(px(6.0))
|
||||||
cx: &mut Context<ElyShell>,
|
.cursor_pointer()
|
||||||
) -> AnyElement {
|
.hover(|style| style.bg(rgba(colors::GLASS_3)))
|
||||||
Button::new(("new-tab-action", index))
|
.active(|style| style.opacity(0.82))
|
||||||
.ghost()
|
.on_click(cx.listener(move |shell, _, window, cx| {
|
||||||
.small()
|
shell.open_internal_tab(route, window, cx);
|
||||||
.icon(action.icon.clone())
|
}))
|
||||||
.label(action.label)
|
.child(
|
||||||
.tooltip(action.label)
|
div()
|
||||||
.on_click(cx.listener(move |shell, _, window, cx| {
|
.text_color(rgb(colors::INK_3))
|
||||||
shell.open_internal_tab(action.route, window, cx);
|
.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()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_new_tab_metrics(snapshot: &BrowserSnapshot) -> AnyElement {
|
fn render_favorites_grid(snapshot: &BrowserSnapshot, cx: &mut Context<ElyShell>) -> AnyElement {
|
||||||
div()
|
if snapshot.favorites.is_empty() {
|
||||||
.border_t_1()
|
return div().into_any_element();
|
||||||
.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 new_tab_metric(label: &'static str, value: usize) -> AnyElement {
|
|
||||||
div()
|
div()
|
||||||
.min_w_0()
|
|
||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
.gap_1()
|
.gap(px(12.0))
|
||||||
.child(div().text_xs().text_color(rgb(colors::MUTED)).child(label))
|
|
||||||
.child(
|
.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<ElyShell>) -> AnyElement {
|
|
||||||
div()
|
|
||||||
.flex_1()
|
|
||||||
.min_h_0()
|
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.gap_3()
|
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.flex_wrap()
|
||||||
.justify_between()
|
.gap(px(10.0))
|
||||||
.gap_3()
|
.children(snapshot.favorites.iter().enumerate().map(|(index, tab)| {
|
||||||
.child(div().text_sm().font_semibold().text_color(rgb(colors::INK)).child("Tabs"))
|
render_favorite_card(index, tab, cx)
|
||||||
.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)
|
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_new_tab_tab_row(
|
fn render_favorite_card(
|
||||||
|
index: usize,
|
||||||
|
tab: &BrowserTab,
|
||||||
|
cx: &mut Context<ElyShell>,
|
||||||
|
) -> 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<ElyShell>) -> 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,
|
index: usize,
|
||||||
tab: &BrowserTab,
|
tab: &BrowserTab,
|
||||||
active_tab_id: &TabId,
|
active_tab_id: &TabId,
|
||||||
@@ -189,58 +258,50 @@ fn render_new_tab_tab_row(
|
|||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
let tab_id = tab.id().clone();
|
let tab_id = tab.id().clone();
|
||||||
let active = tab.id() == active_tab_id;
|
let active = tab.id() == active_tab_id;
|
||||||
let icon = if active { IconName::CircleCheck } else { IconName::Globe };
|
let bg = if active { colors::GLASS_3 } else { 0x00000000 };
|
||||||
let icon_color = if active { colors::PRIMARY } else { colors::MUTED_SOFT };
|
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.id(SharedString::from(format!("new-tab-row-{index}")))
|
.id(SharedString::from(format!("tab-row-{index}")))
|
||||||
.py_3()
|
.rounded(px(8.0))
|
||||||
.border_b_1()
|
.px(px(10.0))
|
||||||
.border_color(rgb(colors::HAIRLINE))
|
.py(px(8.0))
|
||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
.justify_between()
|
.gap(px(10.0))
|
||||||
.gap_4()
|
|
||||||
.cursor_pointer()
|
.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))
|
.active(|style| style.opacity(0.82))
|
||||||
.on_click(cx.listener(move |shell, _, window, cx| {
|
.on_click(cx.listener(move |shell, _, window, cx| {
|
||||||
shell.select_tab(&tab_id, 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(
|
.child(
|
||||||
div()
|
div()
|
||||||
.min_w_0()
|
.min_w_0()
|
||||||
|
.flex_1()
|
||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.flex_col()
|
||||||
.gap_3()
|
.gap_1()
|
||||||
.child(div().text_color(rgb(icon_color)).child(icon))
|
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.min_w_0()
|
.text_size(px(13.0))
|
||||||
.flex()
|
.font_weight(gpui::FontWeight(500.0))
|
||||||
.flex_col()
|
.text_color(rgb(colors::INK))
|
||||||
.gap_1()
|
.truncate()
|
||||||
.child(
|
.child(tab.title().to_string()),
|
||||||
div()
|
)
|
||||||
.text_sm()
|
.child(
|
||||||
.font_semibold()
|
div()
|
||||||
.truncate()
|
.text_size(px(11.0))
|
||||||
.text_color(rgb(colors::INK))
|
.text_color(rgb(colors::INK_4))
|
||||||
.child(tab.title().to_string()),
|
.truncate()
|
||||||
)
|
.child(tab.display_url()),
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_xs()
|
|
||||||
.truncate()
|
|
||||||
.text_color(rgb(colors::MUTED))
|
|
||||||
.child(tab.display_url()),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(div().text_xs().text_color(rgb(colors::MUTED)).child(if active {
|
|
||||||
"Active"
|
|
||||||
} else {
|
|
||||||
"Open"
|
|
||||||
}))
|
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user