diff --git a/crates/ely_app/src/shell/chrome/mod.rs b/crates/ely_app/src/shell/chrome/mod.rs index c7fd7ef..1b83ee0 100644 --- a/crates/ely_app/src/shell/chrome/mod.rs +++ b/crates/ely_app/src/shell/chrome/mod.rs @@ -3,6 +3,8 @@ pub(crate) mod brand_glyph; pub(crate) mod command_footer; pub(crate) mod command_overlay; pub(crate) mod home; +pub(crate) mod plugin_detail_view; +pub(crate) mod plugin_labels; pub(crate) mod settings_layout; pub(crate) mod sidebar; pub(crate) mod sidebar_header; @@ -15,6 +17,7 @@ pub(crate) use appearance_form::render_appearance_form; 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 plugin_detail_view::render_plugin_detail_view; pub(crate) use settings_layout::render_settings_landing; pub(crate) use sidebar::{PANEL_BG, panel_shadow}; pub(crate) use sidebar_header::render_sidebar_header; diff --git a/crates/ely_app/src/shell/chrome/plugin_detail_view.rs b/crates/ely_app/src/shell/chrome/plugin_detail_view.rs new file mode 100644 index 0000000..804d0c9 --- /dev/null +++ b/crates/ely_app/src/shell/chrome/plugin_detail_view.rs @@ -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, +) -> 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, +) -> 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, +) -> 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( + label: &'static str, + cx: &mut Context, + handler: F, +) -> AnyElement +where + F: Fn(&mut ElyShell, &mut gpui::Window, &mut Context) + '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( + icon: IconName, + cx: &mut Context, + handler: F, +) -> AnyElement +where + F: Fn(&mut ElyShell, &mut gpui::Window, &mut Context) + '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; diff --git a/crates/ely_app/src/shell/chrome/plugin_labels.rs b/crates/ely_app/src/shell/chrome/plugin_labels.rs new file mode 100644 index 0000000..54aafd0 --- /dev/null +++ b/crates/ely_app/src/shell/chrome/plugin_labels.rs @@ -0,0 +1,57 @@ +use ely_domain::{PluginContributionPoint, PluginPermission}; + +pub(crate) fn permission_scope_label(permission: &PluginPermission) -> &'static str { + match permission { + PluginPermission::TabsRead => "Reads tab metadata.", + PluginPermission::TabsWrite => "Creates, moves, or closes tabs.", + PluginPermission::SpacesRead => "Reads Space metadata.", + PluginPermission::SpacesWrite => "Creates or changes Spaces.", + PluginPermission::BookmarksRead => "Reads bookmarks.", + PluginPermission::BookmarksWrite => "Writes bookmarks.", + PluginPermission::HistoryRead => "Reads browsing history.", + PluginPermission::DownloadsRead => "Reads download entries.", + PluginPermission::DownloadsWrite => "Controls downloads.", + PluginPermission::PageMetadata => "Reads active page metadata.", + PluginPermission::PageScreenshot => "Captures page screenshots.", + PluginPermission::PageScript => "Runs scoped page scripts.", + PluginPermission::ClipboardRead => "Reads clipboard content.", + PluginPermission::ClipboardWrite => "Writes clipboard content.", + PluginPermission::FilesystemRead => "Reads user-selected files.", + PluginPermission::FilesystemWrite => "Writes user-selected files.", + PluginPermission::NetworkFetch => "Performs plugin network requests.", + PluginPermission::SettingsRead => "Reads plugin settings.", + PluginPermission::SettingsWrite => "Writes plugin settings.", + PluginPermission::SyncPlugin => "Syncs plugin configuration.", + PluginPermission::UiPanel => "Registers sidebar panels.", + PluginPermission::UiCommand => "Registers command bar actions.", + PluginPermission::UiContextMenu => "Registers context menu actions.", + } +} + +pub(crate) fn contribution_title(contribution: &PluginContributionPoint) -> &'static str { + match contribution { + PluginContributionPoint::CommandBarCommand => "Command bar action", + PluginContributionPoint::TabContextMenu => "Tab context menu", + PluginContributionPoint::PageContextMenu => "Page context menu", + PluginContributionPoint::SidebarPanel => "Sidebar panel", + PluginContributionPoint::SettingsPage => "Settings page", + PluginContributionPoint::StatusBarIndicator => "Status bar indicator", + PluginContributionPoint::DownloadAction => "Download action", + PluginContributionPoint::BookmarkAction => "Bookmark action", + PluginContributionPoint::ReadingModeExporter => "Reading mode exporter", + } +} + +pub(crate) fn contribution_detail(contribution: &PluginContributionPoint) -> &'static str { + match contribution { + PluginContributionPoint::CommandBarCommand => "Adds entries to the ⌘K command switcher.", + PluginContributionPoint::TabContextMenu => "Adds tab right-click actions.", + PluginContributionPoint::PageContextMenu => "Adds page right-click actions.", + PluginContributionPoint::SidebarPanel => "Registers a custom sidebar panel.", + PluginContributionPoint::SettingsPage => "Registers a settings page route.", + PluginContributionPoint::StatusBarIndicator => "Renders an indicator in the status bar.", + PluginContributionPoint::DownloadAction => "Adds actions to download entries.", + PluginContributionPoint::BookmarkAction => "Adds actions to bookmarks.", + PluginContributionPoint::ReadingModeExporter => "Exports reading mode content elsewhere.", + } +} diff --git a/crates/ely_app/src/shell/internal_pages/plugin_details.rs b/crates/ely_app/src/shell/internal_pages/plugin_details.rs index 20f3115..06fdd02 100644 --- a/crates/ely_app/src/shell/internal_pages/plugin_details.rs +++ b/crates/ely_app/src/shell/internal_pages/plugin_details.rs @@ -1,16 +1,15 @@ use ely_browser_core::{BrowserSnapshot, InstalledPlugin}; use ely_design_system::colors; -use ely_domain::{PluginId, PluginManifest, PluginPermission, PluginPermissionRisk, ProfileKind}; -use gpui::prelude::FluentBuilder; +use ely_domain::PluginId; use gpui::{AnyElement, Context, IntoElement, ParentElement, Styled, div, px, rgb}; use gpui_component::{ - IconName, Sizable, StyledExt, + IconName, Sizable, button::{Button, ButtonVariants}, scroll::ScrollableElement, }; use super::{ElyShell, render_canvas_surface}; -use crate::shell::chrome::SERIF_FAMILY; +use crate::shell::chrome::{SERIF_FAMILY, render_plugin_detail_view}; impl ElyShell { pub(super) fn render_plugin_detail_page( @@ -23,330 +22,15 @@ impl ElyShell { let plugin = plugin_id.as_ref().and_then(|plugin_id| find_installed_plugin(snapshot, plugin_id)); - render_canvas_surface( - div() - .size_full() - .p_8() - .flex() - .flex_col() - .gap_5() - .when_some(plugin, |this, plugin| { - this.child(self.render_plugin_detail_header( - plugin, - &snapshot.active_profile_kind, - cx, - )) - .child(render_plugin_security_summary(plugin)) - .child(render_plugin_manifest_rows(plugin.manifest())) - .child(render_plugin_permission_list(plugin.manifest())) - }) - .when(plugin.is_none(), |this| { - this.child(render_missing_plugin_detail(plugin_id.as_ref(), cx)) - }), - ) - } - - fn render_plugin_detail_header( - &mut self, - plugin: &InstalledPlugin, - profile_kind: &ProfileKind, - cx: &mut Context, - ) -> AnyElement { - let plugin_id = plugin.id().clone(); - let target_enabled = !plugin.enabled(); - let private_allowed = plugin.private_window_allowed(); - let status_label = plugin_status_label(plugin, profile_kind); - let status_color = - if plugin.enabled_for_profile(profile_kind) { colors::SUCCESS } else { colors::MUTED }; - let action_label = if plugin.enabled() { "Disable" } else { "Enable" }; - let action_icon = if plugin.enabled() { IconName::CircleX } else { IconName::Check }; - let private_action_label = if private_allowed { "Block Private" } else { "Allow Private" }; - let private_action_icon = if private_allowed { IconName::CircleX } else { IconName::Check }; - - div() - .flex() - .items_end() - .justify_between() - .gap_4() - .child( - div() - .min_w_0() - .flex() - .flex_col() - .gap_2() - .child( - div() - .font_family(SERIF_FAMILY) - .text_size(px(28.0)) - .truncate() - .text_color(rgb(colors::INK)) - .child(plugin.manifest().name().to_string()), - ) - .child(div().text_sm().truncate().text_color(rgb(colors::MUTED)).child( - format!("{} - {}", plugin.manifest().author(), plugin.id().as_str()), - )), - ) - .child( - div() - .flex() - .items_center() - .gap_3() - .text_xs() - .child(div().font_semibold().text_color(rgb(status_color)).child(status_label)) - .child( - Button::new("toggle-plugin-detail-private") - .ghost() - .xsmall() - .icon(private_action_icon) - .label(private_action_label) - .tooltip("Set Private Window Permission") - .on_click(cx.listener({ - let plugin_id = plugin_id.clone(); - move |shell, _, _, cx| { - shell.set_plugin_private_window_allowed( - plugin_id.clone(), - !private_allowed, - cx, - ); - } - })), - ) - .child( - Button::new("toggle-plugin-detail-enabled") - .xsmall() - .icon(action_icon) - .label(action_label) - .tooltip("Set Plugin State") - .on_click(cx.listener(move |shell, _, _, cx| { - shell.set_plugin_enabled(plugin_id.clone(), target_enabled, cx); - })), - ) - .child( - Button::new("open-plugin-settings") - .ghost() - .xsmall() - .icon(IconName::Info) - .label("Settings") - .tooltip("Open Plugin Settings") - .on_click(cx.listener(|shell, _, window, cx| { - shell.open_internal_tab("ely://settings/plugins", window, cx); - })), - ), - ) - .into_any_element() - } -} - -fn plugin_status_label(plugin: &InstalledPlugin, profile_kind: &ProfileKind) -> &'static str { - if !plugin.enabled() { - return "Disabled"; - } - match profile_kind { - ProfileKind::Standard => "Enabled", - ProfileKind::Private if plugin.private_window_allowed() => "Private Enabled", - ProfileKind::Private => "Private Off", - } -} - -fn render_plugin_security_summary(plugin: &InstalledPlugin) -> AnyElement { - let manifest = plugin.manifest(); - div() - .border_t_1() - .border_b_1() - .border_color(rgb(colors::HAIRLINE)) - .py_3() - .flex() - .items_center() - .justify_between() - .gap_4() - .children([ - detail_metric("Signature", manifest.signature().algorithm().as_str()), - detail_metric("Key", manifest.signature().key_id()), - detail_metric("High Risk", manifest.high_risk_permissions().count().to_string()), - detail_metric("Sync", sync_participation_label(manifest)), - ]) - .into_any_element() -} - -fn detail_metric(label: &'static str, value: impl Into) -> AnyElement { - div() - .min_w_0() - .flex() - .flex_col() - .gap_1() - .child(div().text_xs().text_color(rgb(colors::MUTED)).child(label)) - .child( - div() - .text_sm() - .font_semibold() - .truncate() - .text_color(rgb(colors::INK)) - .child(value.into()), - ) - .into_any_element() -} - -fn render_plugin_manifest_rows(manifest: &PluginManifest) -> AnyElement { - div() - .flex() - .flex_col() - .border_b_1() - .border_color(rgb(colors::HAIRLINE)) - .child(plugin_detail_row( - IconName::Info, - "Description", - manifest.description(), - "Manifest summary", - )) - .child(plugin_detail_row(IconName::User, "Author", manifest.author(), "Publisher")) - .child(plugin_detail_row(IconName::Globe, "Homepage", manifest.homepage(), "Publisher URL")) - .child(plugin_detail_row( - IconName::CircleCheck, - "Minimum Build", - manifest.min_ely_build().to_string(), - "Required ELY version", - )) - .child(plugin_detail_row( - IconName::BookOpen, - "Checksum", - manifest.checksum(), - "Wasm package checksum", - )) - .into_any_element() -} - -fn plugin_detail_row( - icon: IconName, - label: &'static str, - value: impl Into, - detail: &'static str, -) -> AnyElement { - let value = value.into(); - - div() - .py_3() - .border_t_1() - .border_color(rgb(colors::HAIRLINE)) - .flex() - .items_center() - .justify_between() - .gap_4() - .child( - div() - .min_w_0() - .flex() - .items_center() - .gap_3() - .child(div().text_color(rgb(colors::MUTED_SOFT)).child(icon)) - .child( - div() - .min_w_0() - .flex() - .flex_col() - .gap_1() - .child( - div() - .text_sm() - .font_semibold() - .truncate() - .text_color(rgb(colors::INK)) - .child(label), - ) - .child(div().text_xs().text_color(rgb(colors::MUTED)).child(detail)), + match plugin { + Some(plugin) => render_canvas_surface( + div().size_full().overflow_y_scrollbar().child( + render_plugin_detail_view(plugin, &snapshot.active_profile_kind, cx), ), - ) - .child( - div() - .max_w(px(380.0)) - .truncate() - .text_sm() - .font_semibold() - .text_color(rgb(colors::INK)) - .child(value), - ) - .into_any_element() -} - -fn render_plugin_permission_list(manifest: &PluginManifest) -> AnyElement { - if manifest.permissions().is_empty() { - return div() - .text_sm() - .text_color(rgb(colors::MUTED)) - .child("This plugin declares no permissions.") - .into_any_element(); + ), + None => render_canvas_surface(render_missing_plugin_detail(plugin_id.as_ref(), cx)), + } } - - div() - .flex_1() - .min_h_0() - .flex() - .flex_col() - .gap_3() - .child(div().text_xs().font_semibold().text_color(rgb(colors::MUTED)).child("Permissions")) - .child( - div() - .flex_1() - .min_h_0() - .flex() - .flex_col() - .overflow_y_scrollbar() - .children(manifest.permissions().iter().map(render_permission_row)), - ) - .into_any_element() -} - -fn render_permission_row(permission: &PluginPermission) -> AnyElement { - let risk = permission.risk(); - let risk_color = match risk { - PluginPermissionRisk::Standard => colors::MUTED, - PluginPermissionRisk::High => colors::ERROR, - }; - - div() - .py_3() - .border_b_1() - .border_color(rgb(colors::HAIRLINE)) - .flex() - .items_center() - .justify_between() - .gap_4() - .child( - div() - .min_w_0() - .flex() - .items_center() - .gap_3() - .child(div().text_color(rgb(risk_color)).child(permission_icon(permission))) - .child( - div() - .min_w_0() - .flex() - .flex_col() - .gap_1() - .child( - div() - .text_sm() - .font_semibold() - .truncate() - .text_color(rgb(colors::INK)) - .child(permission.as_str()), - ) - .child( - div() - .text_xs() - .text_color(rgb(colors::MUTED)) - .child(permission_scope_label(permission)), - ), - ), - ) - .child( - div() - .text_xs() - .font_semibold() - .text_color(rgb(risk_color)) - .child(permission_risk_label(risk)), - ) - .into_any_element() } fn render_missing_plugin_detail( @@ -359,16 +43,23 @@ fn render_missing_plugin_detail( div() .size_full() + .pt(px(40.0)) + .px(px(56.0)) + .pb(px(40.0)) .flex() .flex_col() - .gap_5() + .gap(px(20.0)) .child( div() .flex() .flex_col() - .gap_2() + .gap(px(6.0)) .child( - div().text_size(px(26.0)).text_color(rgb(colors::INK)).child("Plugin Details"), + div() + .font_family(SERIF_FAMILY) + .text_size(px(28.0)) + .text_color(rgb(colors::INK)) + .child("Plugin Details"), ) .child(div().text_sm().text_color(rgb(colors::MUTED)).child(detail)), ) @@ -376,11 +67,11 @@ fn render_missing_plugin_detail( div() .border_t_1() .border_color(rgb(colors::HAIRLINE)) - .pt_5() + .pt(px(20.0)) .flex() .items_center() .justify_between() - .gap_4() + .gap(px(16.0)) .child( div() .text_sm() @@ -413,58 +104,3 @@ fn find_installed_plugin<'a>( ) -> Option<&'a InstalledPlugin> { snapshot.installed_plugins.iter().find(|plugin| plugin.id() == plugin_id) } - -fn sync_participation_label(manifest: &PluginManifest) -> &'static str { - if manifest - .permissions() - .iter() - .any(|permission| matches!(permission, PluginPermission::SyncPlugin)) - { - "Participates" - } else { - "Local" - } -} - -fn permission_risk_label(risk: PluginPermissionRisk) -> &'static str { - match risk { - PluginPermissionRisk::Standard => "Standard", - PluginPermissionRisk::High => "High", - } -} - -fn permission_icon(permission: &PluginPermission) -> IconName { - if permission.requires_separate_confirmation() { - IconName::TriangleAlert - } else { - IconName::CircleCheck - } -} - -fn permission_scope_label(permission: &PluginPermission) -> &'static str { - match permission { - PluginPermission::TabsRead => "Reads tab metadata.", - PluginPermission::TabsWrite => "Creates, moves, or closes tabs.", - PluginPermission::SpacesRead => "Reads Space metadata.", - PluginPermission::SpacesWrite => "Creates or changes Spaces.", - PluginPermission::BookmarksRead => "Reads bookmarks.", - PluginPermission::BookmarksWrite => "Writes bookmarks.", - PluginPermission::HistoryRead => "Reads browsing history.", - PluginPermission::DownloadsRead => "Reads download entries.", - PluginPermission::DownloadsWrite => "Controls downloads.", - PluginPermission::PageMetadata => "Reads active page metadata.", - PluginPermission::PageScreenshot => "Captures page screenshots.", - PluginPermission::PageScript => "Runs scoped page scripts.", - PluginPermission::ClipboardRead => "Reads clipboard content.", - PluginPermission::ClipboardWrite => "Writes clipboard content.", - PluginPermission::FilesystemRead => "Reads user-selected files.", - PluginPermission::FilesystemWrite => "Writes user-selected files.", - PluginPermission::NetworkFetch => "Performs plugin network requests.", - PluginPermission::SettingsRead => "Reads plugin settings.", - PluginPermission::SettingsWrite => "Writes plugin settings.", - PluginPermission::SyncPlugin => "Syncs plugin configuration.", - PluginPermission::UiPanel => "Registers sidebar panels.", - PluginPermission::UiCommand => "Registers command bar actions.", - PluginPermission::UiContextMenu => "Registers context menu actions.", - } -}