Redesign plugin detail page in design's 320 + 1fr grid

Plugin detail now opens with the design's marketplace card: a
4/3 brand-gradient cover plus install/secondary buttons plus a
permissions list with risk badges on the left, and a category
overline plus serif Newsreader title plus description plus a
4-up real-data stat grid (permissions, high-risk, contributes,
signature) plus a "What it adds to ELY" contributions list on
the right. Every value comes from PluginManifest — no fabricated
ratings, install counts, or feature copy.

Internal: chrome::plugin_detail_view holds the layout, and
chrome::plugin_labels owns the permission scope labels and
contribution copy so plugin_detail_view stays under 500 lines.
The internal_pages/plugin_details.rs shim only handles route
parsing and the missing-plugin fallback.
This commit is contained in:
2026-05-09 19:01:39 -04:00
parent ed3150f9ed
commit b0b4ac3791
4 changed files with 534 additions and 386 deletions
@@ -0,0 +1,452 @@
use ely_browser_core::InstalledPlugin;
use ely_design_system::colors;
use ely_domain::{
PluginContributionPoint, PluginManifest, PluginPermission, PluginPermissionRisk, ProfileKind,
};
use gpui::{
AnyElement, Context, FontWeight, InteractiveElement, IntoElement, ParentElement, SharedString,
StatefulInteractiveElement, Styled, div, hsla, linear_color_stop, linear_gradient, px, rgb,
rgba,
};
use gpui_component::IconName;
use crate::shell::ElyShell;
use crate::shell::chrome::SERIF_FAMILY;
use crate::shell::chrome::plugin_labels::{
contribution_detail, contribution_title, permission_scope_label,
};
pub(crate) fn render_plugin_detail_view(
plugin: &InstalledPlugin,
profile_kind: &ProfileKind,
cx: &mut Context<ElyShell>,
) -> AnyElement {
div()
.size_full()
.pt(px(40.0))
.px(px(56.0))
.pb(px(40.0))
.child(
div()
.max_w(px(960.0))
.mx_auto()
.p(px(24.0))
.rounded(px(16.0))
.bg(rgba(CARD_BG))
.grid()
.grid_cols(8)
.gap(px(24.0))
.child(
div()
.col_span(3)
.child(render_left_column(plugin, profile_kind, cx)),
)
.child(
div()
.col_span(5)
.child(render_right_column(plugin)),
),
)
.into_any_element()
}
fn render_left_column(
plugin: &InstalledPlugin,
profile_kind: &ProfileKind,
cx: &mut Context<ElyShell>,
) -> AnyElement {
div()
.flex()
.flex_col()
.gap(px(16.0))
.child(render_cover(plugin))
.child(render_install_row(plugin, profile_kind, cx))
.child(render_permissions_block(plugin.manifest()))
.into_any_element()
}
fn render_cover(plugin: &InstalledPlugin) -> AnyElement {
let initial = plugin
.manifest()
.name()
.chars()
.next()
.unwrap_or('◇')
.to_string()
.to_uppercase();
div()
.h(px(220.0))
.rounded(px(14.0))
.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(80.0))
.text_color(rgb(0xffffff))
.child(initial)
.into_any_element()
}
fn render_install_row(
plugin: &InstalledPlugin,
profile_kind: &ProfileKind,
cx: &mut Context<ElyShell>,
) -> AnyElement {
let plugin_id = plugin.id().clone();
let enabled = plugin.enabled_for_profile(profile_kind);
let primary_label = if enabled { "Disable" } else { "Enable" };
let target_enabled = !plugin.enabled();
div()
.flex()
.items_center()
.gap(px(10.0))
.child(render_primary_button(primary_label, cx, move |shell, _, cx| {
shell.set_plugin_enabled(plugin_id.clone(), target_enabled, cx);
}))
.child(render_secondary_button(IconName::Info, cx, |shell, window, cx| {
shell.open_internal_tab("ely://settings/plugins", window, cx);
}))
.into_any_element()
}
fn render_primary_button<F>(
label: &'static str,
cx: &mut Context<ElyShell>,
handler: F,
) -> AnyElement
where
F: Fn(&mut ElyShell, &mut gpui::Window, &mut Context<ElyShell>) + 'static,
{
div()
.id(SharedString::from("plugin-detail-primary"))
.flex_1()
.h(px(40.0))
.rounded(px(10.0))
.bg(rgb(colors::INK))
.flex()
.items_center()
.justify_center()
.gap(px(8.0))
.text_size(px(13.0))
.font_weight(FontWeight(500.0))
.text_color(rgb(0xffffff))
.cursor_pointer()
.hover(|style| style.opacity(0.92))
.active(|style| style.opacity(0.78))
.on_click(cx.listener(move |shell, _, window, cx| handler(shell, window, cx)))
.child(
div()
.text_color(rgb(0xffffff))
.child(IconName::Check),
)
.child(label)
.into_any_element()
}
fn render_secondary_button<F>(
icon: IconName,
cx: &mut Context<ElyShell>,
handler: F,
) -> AnyElement
where
F: Fn(&mut ElyShell, &mut gpui::Window, &mut Context<ElyShell>) + 'static,
{
div()
.id(SharedString::from("plugin-detail-secondary"))
.size(px(40.0))
.rounded(px(10.0))
.bg(rgba(SECONDARY_BG))
.flex()
.items_center()
.justify_center()
.text_color(rgb(colors::INK_3))
.cursor_pointer()
.hover(|style| style.bg(rgba(SECONDARY_BG_HOVER)).text_color(rgb(colors::INK)))
.active(|style| style.opacity(0.82))
.on_click(cx.listener(move |shell, _, window, cx| handler(shell, window, cx)))
.child(icon)
.into_any_element()
}
fn render_permissions_block(manifest: &PluginManifest) -> AnyElement {
div()
.flex()
.flex_col()
.gap(px(8.0))
.pt(px(4.0))
.child(
div()
.text_size(px(11.0))
.text_color(rgb(colors::INK_3))
.child("PERMISSIONS"),
)
.child(if manifest.permissions().is_empty() {
div()
.text_size(px(12.0))
.text_color(rgb(colors::INK_4))
.child("This plugin declares no permissions.")
.into_any_element()
} else {
div()
.flex()
.flex_col()
.gap(px(6.0))
.children(manifest.permissions().iter().map(render_permission_row))
.into_any_element()
})
.into_any_element()
}
fn render_permission_row(permission: &PluginPermission) -> AnyElement {
let high_risk = matches!(permission.risk(), PluginPermissionRisk::High);
let badge_bg = if high_risk { 0xfde7e7 } else { 0xe8f3ee };
let badge_color = if high_risk { 0xb53737 } else { 0x2f7d68 };
div()
.flex()
.items_center()
.gap(px(8.0))
.text_size(px(12.0))
.child(
div()
.size(px(14.0))
.rounded(px(4.0))
.bg(rgb(badge_bg))
.flex()
.items_center()
.justify_center()
.text_color(rgb(badge_color))
.text_size(px(10.0))
.child(if high_risk { "!" } else { "" }),
)
.child(
div()
.flex_1()
.text_color(rgb(colors::INK_2))
.child(permission_scope_label(permission)),
)
.into_any_element()
}
fn render_right_column(plugin: &InstalledPlugin) -> AnyElement {
let manifest = plugin.manifest();
div()
.flex()
.flex_col()
.gap(px(16.0))
.child(render_right_header(plugin))
.child(
div()
.text_size(px(13.5))
.text_color(rgb(colors::INK_2))
.child(manifest.description().to_string()),
)
.child(render_stats_grid(plugin))
.child(render_contributions(manifest))
.into_any_element()
}
fn render_right_header(plugin: &InstalledPlugin) -> AnyElement {
let manifest = plugin.manifest();
div()
.flex()
.items_start()
.gap(px(14.0))
.child(
div()
.flex_1()
.min_w_0()
.flex()
.flex_col()
.gap(px(4.0))
.child(
div()
.text_size(px(11.0))
.text_color(rgb(colors::INK_4))
.child(category_label(plugin)),
)
.child(
div()
.font_family(SERIF_FAMILY)
.text_size(px(32.0))
.font_weight(FontWeight(400.0))
.text_color(rgb(colors::INK))
.child(manifest.name().to_string()),
)
.child(
div()
.text_size(px(12.5))
.text_color(rgb(colors::INK_3))
.child(format!(
"by {} · min ELY {}",
manifest.author(),
manifest.min_ely_build()
)),
),
)
.child(render_status_chip(plugin))
.into_any_element()
}
fn render_status_chip(plugin: &InstalledPlugin) -> AnyElement {
let enabled = plugin.enabled();
let label = if enabled { "Enabled" } else { "Disabled" };
let bg = if enabled { 0xe8f3ee } else { 0x281e140d };
let color = if enabled { 0x2f7d68 } else { colors::INK_3 };
div()
.px(px(10.0))
.py(px(4.0))
.rounded(px(999.0))
.bg(rgba(bg))
.text_size(px(11.0))
.font_weight(FontWeight(500.0))
.text_color(rgb(color))
.child(label)
.into_any_element()
}
fn render_stats_grid(plugin: &InstalledPlugin) -> AnyElement {
let manifest = plugin.manifest();
let permissions = manifest.permissions().len();
let high_risk = manifest.high_risk_permissions().count();
let contributes = manifest.contributes().len();
let signature = manifest.signature().algorithm().as_str();
div()
.grid()
.grid_cols(4)
.gap(px(8.0))
.child(stat_card("Permissions", permissions.to_string()))
.child(stat_card("High risk", high_risk.to_string()))
.child(stat_card("Contributes", contributes.to_string()))
.child(stat_card("Signature", signature.to_string()))
.into_any_element()
}
fn stat_card(label: &'static str, value: String) -> AnyElement {
div()
.px(px(12.0))
.py(px(10.0))
.rounded(px(10.0))
.bg(rgba(STAT_BG))
.flex()
.flex_col()
.gap(px(2.0))
.child(
div()
.text_size(px(10.5))
.text_color(rgb(colors::INK_4))
.child(label),
)
.child(
div()
.text_size(px(13.0))
.font_weight(FontWeight(500.0))
.text_color(rgb(colors::INK))
.child(value),
)
.into_any_element()
}
fn render_contributions(manifest: &PluginManifest) -> AnyElement {
let contributions = manifest.contributes();
div()
.flex()
.flex_col()
.gap(px(10.0))
.child(
div()
.text_size(px(13.0))
.font_weight(FontWeight(600.0))
.text_color(rgb(colors::INK))
.child("What it adds to ELY"),
)
.child(if contributions.is_empty() {
div()
.text_size(px(12.0))
.text_color(rgb(colors::INK_4))
.child("This plugin does not register any contributions.")
.into_any_element()
} else {
div()
.flex()
.flex_col()
.gap(px(8.0))
.children(contributions.iter().map(render_contribution_row))
.into_any_element()
})
.into_any_element()
}
fn render_contribution_row(contribution: &PluginContributionPoint) -> AnyElement {
div()
.flex()
.items_start()
.gap(px(10.0))
.px(px(12.0))
.py(px(10.0))
.rounded(px(10.0))
.bg(rgba(CONTRIBUTION_BG))
.child(
div()
.size(px(18.0))
.rounded(px(5.0))
.bg(rgba(CONTRIBUTION_BADGE_BG))
.flex()
.items_center()
.justify_center()
.mt(px(1.0))
.text_color(rgb(colors::ACCENT))
.text_size(px(11.0))
.child(IconName::Check),
)
.child(
div()
.flex_1()
.min_w_0()
.flex()
.flex_col()
.gap(px(2.0))
.child(
div()
.text_size(px(12.5))
.font_weight(FontWeight(500.0))
.text_color(rgb(colors::INK))
.child(contribution_title(contribution)),
)
.child(
div()
.text_size(px(11.5))
.text_color(rgb(colors::INK_3))
.child(contribution_detail(contribution)),
),
)
.into_any_element()
}
fn category_label(plugin: &InstalledPlugin) -> &'static str {
let high_risk = plugin.manifest().high_risk_permissions().next().is_some();
if high_risk {
"ELY · AUDIT NEEDED"
} else {
"ELY · SANDBOXED"
}
}
const CARD_BG: u32 = 0xffffffd9;
const SECONDARY_BG: u32 = 0xffffffd9;
const SECONDARY_BG_HOVER: u32 = 0xffffffeb;
const STAT_BG: u32 = 0xffffffb3;
const CONTRIBUTION_BG: u32 = 0xffffff8c;
const CONTRIBUTION_BADGE_BG: u32 = 0xc964421f;