diff --git a/crates/ely_app/src/shell/chrome/brand_glyph.rs b/crates/ely_app/src/shell/chrome/brand_glyph.rs new file mode 100644 index 0000000..95cea31 --- /dev/null +++ b/crates/ely_app/src/shell/chrome/brand_glyph.rs @@ -0,0 +1,296 @@ +use ely_design_system::colors; +use gpui::{ + AnyElement, FontWeight, IntoElement, ParentElement, Styled, div, hsla, linear_color_stop, + linear_gradient, px, rgb, rgba, +}; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum Brand { + Notion, + YouTube, + Dribbble, + X, + Vercel, + Behance, + Figma, + Slack, + GitHub, + Linear, + Reading, + News, +} + +impl Brand { + pub(crate) fn from_host(host: &str) -> Option { + let normalized = host.trim().to_lowercase(); + let normalized = normalized.strip_prefix("www.").unwrap_or(&normalized); + + if normalized.ends_with("notion.so") || normalized.ends_with("notion.com") { + return Some(Self::Notion); + } + if normalized.ends_with("youtube.com") || normalized.ends_with("youtu.be") { + return Some(Self::YouTube); + } + if normalized.ends_with("dribbble.com") { + return Some(Self::Dribbble); + } + if normalized.ends_with("x.com") || normalized.ends_with("twitter.com") { + return Some(Self::X); + } + if normalized.ends_with("vercel.com") { + return Some(Self::Vercel); + } + if normalized.ends_with("behance.net") { + return Some(Self::Behance); + } + if normalized.ends_with("figma.com") { + return Some(Self::Figma); + } + if normalized.ends_with("slack.com") { + return Some(Self::Slack); + } + if normalized.ends_with("github.com") || normalized.ends_with("github.io") { + return Some(Self::GitHub); + } + if normalized.ends_with("linear.app") { + return Some(Self::Linear); + } + if normalized.ends_with("medium.com") + || normalized.ends_with("substack.com") + || normalized.ends_with("nytimes.com") + || normalized.ends_with("ft.com") + { + return Some(Self::News); + } + if normalized.ends_with("readwise.io") + || normalized.ends_with("instapaper.com") + || normalized.ends_with("pocket.com") + { + return Some(Self::Reading); + } + None + } +} + +pub(crate) fn render_brand_glyph(brand: Brand, size: f32) -> AnyElement { + match brand { + Brand::Notion => render_notion(size), + Brand::YouTube => render_youtube(size), + Brand::Dribbble => render_dribbble(size), + Brand::X => render_x(size), + Brand::Vercel => render_vercel(size), + Brand::Behance => render_behance(size), + Brand::Figma => render_figma(size), + Brand::Slack => render_slack(size), + Brand::GitHub => render_github(size), + Brand::Linear => render_linear(size), + Brand::Reading => render_reading(size), + Brand::News => render_news(size), + } +} + +pub(crate) fn render_glyph_for( + host: Option<&str>, + fallback_initial: &str, + size: f32, +) -> AnyElement { + if let Some(host) = host + && let Some(brand) = Brand::from_host(host) + { + return render_brand_glyph(brand, size); + } + render_fallback(fallback_initial, size) +} + +fn render_notion(size: f32) -> AnyElement { + flat_square(size, 0xffffff, fontable(size, 0.65, 700.0, 0x111111, "N")) +} + +fn render_youtube(size: f32) -> AnyElement { + flat_square(size, 0xff3b2d, fontable(size, 0.55, 700.0, 0xffffff, "▶")) +} + +fn render_dribbble(size: f32) -> AnyElement { + div() + .size(px(size)) + .rounded_full() + .bg(rgb(0xea4c89)) + .flex() + .items_center() + .justify_center() + .text_size(px(size * 0.6)) + .font_weight(FontWeight(800.0)) + .text_color(rgb(0xffffff)) + .child("•") + .into_any_element() +} + +fn render_x(size: f32) -> AnyElement { + flat_square(size, 0x000000, fontable(size, 0.55, 700.0, 0xffffff, "𝕏")) +} + +fn render_vercel(size: f32) -> AnyElement { + flat_square(size, 0xffffff, fontable(size, 0.55, 700.0, 0x000000, "▲")) +} + +fn render_behance(size: f32) -> AnyElement { + flat_square(size, 0x1769ff, fontable(size, 0.55, 800.0, 0xffffff, "Bē")) +} + +fn render_figma(size: f32) -> AnyElement { + div() + .size(px(size)) + .rounded(px(size * 0.23)) + .bg(linear_gradient( + 135.0, + linear_color_stop(hsla(13.0 / 360.0, 0.88, 0.55, 1.0), 0.0), + linear_color_stop(hsla(266.0 / 360.0, 1.0, 0.67, 1.0), 1.0), + )) + .flex() + .items_center() + .justify_center() + .text_size(px(size * 0.55)) + .font_weight(FontWeight(700.0)) + .text_color(rgb(0xffffff)) + .child("F") + .into_any_element() +} + +fn render_slack(size: f32) -> AnyElement { + div() + .size(px(size)) + .rounded(px(size * 0.23)) + .bg(linear_gradient( + 135.0, + linear_color_stop(hsla(196.0 / 360.0, 0.85, 0.59, 1.0), 0.0), + linear_color_stop(hsla(343.0 / 360.0, 0.78, 0.49, 1.0), 1.0), + )) + .flex() + .items_center() + .justify_center() + .text_size(px(size * 0.55)) + .font_weight(FontWeight(800.0)) + .text_color(rgb(0xffffff)) + .child("#") + .into_any_element() +} + +fn render_github(size: f32) -> AnyElement { + flat_square(size, 0x1d1c1a, fontable(size, 0.6, 700.0, 0xffffff, "◯")) +} + +fn render_linear(size: f32) -> AnyElement { + div() + .size(px(size)) + .rounded_full() + .bg(linear_gradient( + 135.0, + linear_color_stop(hsla(234.0 / 360.0, 0.59, 0.61, 1.0), 0.0), + linear_color_stop(hsla(20.0 / 360.0, 0.05, 0.10, 1.0), 1.0), + )) + .flex() + .items_center() + .justify_center() + .text_size(px(size * 0.55)) + .font_weight(FontWeight(600.0)) + .text_color(rgb(0xffffff)) + .child("L") + .into_any_element() +} + +fn render_reading(size: f32) -> AnyElement { + flat_square(size, 0xfaeed8, fontable(size, 0.55, 600.0, 0xc96442, "¶")) +} + +fn render_news(size: f32) -> AnyElement { + flat_square(size, 0x1d1c1a, fontable(size, 0.55, 700.0, 0xffffff, "N")) +} + +fn render_fallback(initial: &str, size: f32) -> AnyElement { + let glyph = initial + .chars() + .next() + .map(|c| c.to_uppercase().to_string()) + .unwrap_or_else(|| "·".to_string()); + + div() + .size(px(size)) + .rounded(px(size * 0.23)) + .bg(linear_gradient( + 135.0, + linear_color_stop(hsla(341.0 / 360.0, 0.78, 0.85, 1.0), 0.0), + linear_color_stop(hsla(228.0 / 360.0, 1.0, 0.86, 1.0), 1.0), + )) + .flex() + .items_center() + .justify_center() + .text_size(px(size * 0.5)) + .font_weight(FontWeight(600.0)) + .text_color(rgb(colors::INK)) + .child(glyph) + .into_any_element() +} + +fn flat_square(size: f32, fill: u32, content: AnyElement) -> AnyElement { + div() + .size(px(size)) + .rounded(px(size * 0.23)) + .bg(rgb(fill)) + .border_1() + .border_color(rgba(0x0000000f)) + .flex() + .items_center() + .justify_center() + .child(content) + .into_any_element() +} + +fn fontable(size: f32, scale: f32, weight: f32, color: u32, glyph: &str) -> AnyElement { + div() + .text_size(px(size * scale)) + .font_weight(FontWeight(weight)) + .text_color(rgb(color)) + .child(glyph.to_string()) + .into_any_element() +} + +#[cfg(test)] +mod tests { + use super::Brand; + + #[test] + fn matches_canonical_brand_hosts() { + assert_eq!(Brand::from_host("notion.so"), Some(Brand::Notion)); + assert_eq!(Brand::from_host("youtube.com"), Some(Brand::YouTube)); + assert_eq!(Brand::from_host("dribbble.com"), Some(Brand::Dribbble)); + assert_eq!(Brand::from_host("x.com"), Some(Brand::X)); + assert_eq!(Brand::from_host("twitter.com"), Some(Brand::X)); + assert_eq!(Brand::from_host("vercel.com"), Some(Brand::Vercel)); + assert_eq!(Brand::from_host("behance.net"), Some(Brand::Behance)); + assert_eq!(Brand::from_host("figma.com"), Some(Brand::Figma)); + assert_eq!(Brand::from_host("slack.com"), Some(Brand::Slack)); + assert_eq!(Brand::from_host("github.com"), Some(Brand::GitHub)); + assert_eq!(Brand::from_host("linear.app"), Some(Brand::Linear)); + } + + #[test] + fn matches_subdomains_and_www_prefix() { + assert_eq!(Brand::from_host("WWW.figma.com"), Some(Brand::Figma)); + assert_eq!(Brand::from_host("design.figma.com"), Some(Brand::Figma)); + assert_eq!(Brand::from_host("api.github.com"), Some(Brand::GitHub)); + } + + #[test] + fn matches_news_and_reading_clusters() { + assert_eq!(Brand::from_host("medium.com"), Some(Brand::News)); + assert_eq!(Brand::from_host("nytimes.com"), Some(Brand::News)); + assert_eq!(Brand::from_host("readwise.io"), Some(Brand::Reading)); + } + + #[test] + fn rejects_unknown_hosts() { + assert_eq!(Brand::from_host("example.com"), None); + assert_eq!(Brand::from_host(""), None); + } +} + diff --git a/crates/ely_app/src/shell/chrome/home/favorites.rs b/crates/ely_app/src/shell/chrome/home/favorites.rs index d305826..e02f493 100644 --- a/crates/ely_app/src/shell/chrome/home/favorites.rs +++ b/crates/ely_app/src/shell/chrome/home/favorites.rs @@ -2,12 +2,13 @@ use ely_browser_core::BrowserSnapshot; use ely_design_system::colors; use ely_domain::BrowserTab; use gpui::{ - AnyElement, Context, FontWeight, InteractiveElement, IntoElement, ParentElement, SharedString, + AnyElement, Context, InteractiveElement, IntoElement, ParentElement, SharedString, StatefulInteractiveElement, Styled, div, px, rgb, rgba, }; use gpui_component::IconName; use crate::shell::ElyShell; +use crate::shell::chrome::render_glyph_for; use super::section::render_section_chevron_label; use super::style::{ADD_TILE_BG, CARD_BG, CARD_BG_HOVER, card_shadow}; @@ -48,14 +49,9 @@ fn render_favorite_tile( cx: &mut Context, ) -> AnyElement { let tab_id = tab.id().clone(); - let initial = tab - .title() - .chars() - .next() - .unwrap_or('?') - .to_uppercase() - .to_string(); + let host = tab.url().host().map(|host| host.to_string()); let title = tab.title().to_string(); + let initial = title.chars().next().unwrap_or('?').to_string(); div() .id(SharedString::from(format!("fav-tile-{index}"))) @@ -74,19 +70,7 @@ fn render_favorite_tile( .on_click(cx.listener(move |shell, _, window, cx| { shell.select_tab(&tab_id, window, cx); })) - .child( - div() - .size(px(28.0)) - .rounded(px(7.0)) - .bg(rgb(colors::ACCENT)) - .flex() - .items_center() - .justify_center() - .text_size(px(14.0)) - .font_weight(FontWeight(600.0)) - .text_color(rgb(0xffffff)) - .child(initial), - ) + .child(render_glyph_for(host.as_deref(), &initial, 28.0)) .child( div() .text_size(px(11.5)) diff --git a/crates/ely_app/src/shell/chrome/mod.rs b/crates/ely_app/src/shell/chrome/mod.rs index 0830a58..b36dc69 100644 --- a/crates/ely_app/src/shell/chrome/mod.rs +++ b/crates/ely_app/src/shell/chrome/mod.rs @@ -1,3 +1,4 @@ +pub(crate) mod brand_glyph; pub(crate) mod command_overlay; pub(crate) mod home; pub(crate) mod settings_layout; @@ -7,6 +8,7 @@ pub(crate) mod split_pane; pub(crate) mod topbar; pub(crate) mod wallpaper; +pub(crate) use brand_glyph::render_glyph_for; pub(crate) use command_overlay::render_command_overlay; pub(crate) use home::render_home_page; pub(crate) use settings_layout::render_settings_landing;