Add brand glyph module and use it on the home favorites grid
The design distinguishes apps by stylized brand glyphs (Notion N, YouTube ▶, Dribbble dot, X 𝕏, Vercel ▲, Behance Bē, Figma F, Slack #, GitHub ◯, Linear L, plus reading and news clusters). Add chrome::brand_glyph with a host → Brand dispatcher so any place that shows an app icon can route through one helper instead of inventing fallback initials. Wire it into the home favorites tile first — tabs whose host matches a known brand now show the proper glyph; unknown hosts still get the gradient initial fallback. Tests cover canonical hosts, www/subdomain normalization, the news/reading clusters, and unknown rejection.
This commit is contained in:
@@ -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<Self> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -2,12 +2,13 @@ use ely_browser_core::BrowserSnapshot;
|
|||||||
use ely_design_system::colors;
|
use ely_design_system::colors;
|
||||||
use ely_domain::BrowserTab;
|
use ely_domain::BrowserTab;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, Context, FontWeight, InteractiveElement, IntoElement, ParentElement, SharedString,
|
AnyElement, Context, InteractiveElement, IntoElement, ParentElement, SharedString,
|
||||||
StatefulInteractiveElement, Styled, div, px, rgb, rgba,
|
StatefulInteractiveElement, Styled, div, px, rgb, rgba,
|
||||||
};
|
};
|
||||||
use gpui_component::IconName;
|
use gpui_component::IconName;
|
||||||
|
|
||||||
use crate::shell::ElyShell;
|
use crate::shell::ElyShell;
|
||||||
|
use crate::shell::chrome::render_glyph_for;
|
||||||
|
|
||||||
use super::section::render_section_chevron_label;
|
use super::section::render_section_chevron_label;
|
||||||
use super::style::{ADD_TILE_BG, CARD_BG, CARD_BG_HOVER, card_shadow};
|
use super::style::{ADD_TILE_BG, CARD_BG, CARD_BG_HOVER, card_shadow};
|
||||||
@@ -48,14 +49,9 @@ fn render_favorite_tile(
|
|||||||
cx: &mut Context<ElyShell>,
|
cx: &mut Context<ElyShell>,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
let tab_id = tab.id().clone();
|
let tab_id = tab.id().clone();
|
||||||
let initial = tab
|
let host = tab.url().host().map(|host| host.to_string());
|
||||||
.title()
|
|
||||||
.chars()
|
|
||||||
.next()
|
|
||||||
.unwrap_or('?')
|
|
||||||
.to_uppercase()
|
|
||||||
.to_string();
|
|
||||||
let title = tab.title().to_string();
|
let title = tab.title().to_string();
|
||||||
|
let initial = title.chars().next().unwrap_or('?').to_string();
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.id(SharedString::from(format!("fav-tile-{index}")))
|
.id(SharedString::from(format!("fav-tile-{index}")))
|
||||||
@@ -74,19 +70,7 @@ fn render_favorite_tile(
|
|||||||
.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(
|
.child(render_glyph_for(host.as_deref(), &initial, 28.0))
|
||||||
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(
|
.child(
|
||||||
div()
|
div()
|
||||||
.text_size(px(11.5))
|
.text_size(px(11.5))
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
pub(crate) mod brand_glyph;
|
||||||
pub(crate) mod command_overlay;
|
pub(crate) mod command_overlay;
|
||||||
pub(crate) mod home;
|
pub(crate) mod home;
|
||||||
pub(crate) mod settings_layout;
|
pub(crate) mod settings_layout;
|
||||||
@@ -7,6 +8,7 @@ pub(crate) mod split_pane;
|
|||||||
pub(crate) mod topbar;
|
pub(crate) mod topbar;
|
||||||
pub(crate) mod wallpaper;
|
pub(crate) mod wallpaper;
|
||||||
|
|
||||||
|
pub(crate) use brand_glyph::render_glyph_for;
|
||||||
pub(crate) use command_overlay::render_command_overlay;
|
pub(crate) use command_overlay::render_command_overlay;
|
||||||
pub(crate) use home::render_home_page;
|
pub(crate) use home::render_home_page;
|
||||||
pub(crate) use settings_layout::render_settings_landing;
|
pub(crate) use settings_layout::render_settings_landing;
|
||||||
|
|||||||
Reference in New Issue
Block a user