From 2f5c662c4287cbde1a7414151aa9a9a835026900 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Fri, 11 Apr 2025 09:43:57 -0600 Subject: [PATCH] Refine component preview & add serialization (#28545) https://github.com/user-attachments/assets/0be12a9a-f6ce-4eca-90de-6ef01eb41ff9 - Allows the active ComponentPreview page to be restored via serialization - Allows filtering components using a filter input - Updates component example rendering - Updates some components Release Notes: - N/A --------- Co-authored-by: Max Brunsfeld --- Cargo.lock | 6 + crates/component/src/component.rs | 42 +- crates/component_preview/Cargo.toml | 10 +- .../src/component_preview.rs | 387 +++++++++++++++--- crates/component_preview/src/persistence.rs | 73 ++++ crates/ui/src/components/avatar.rs | 72 +--- crates/ui/src/components/keybinding.rs | 2 +- 7 files changed, 468 insertions(+), 124 deletions(-) create mode 100644 crates/component_preview/src/persistence.rs diff --git a/Cargo.lock b/Cargo.lock index bfc32cbb24..4ce3131d66 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3175,14 +3175,20 @@ dependencies = [ name = "component_preview" version = "0.1.0" dependencies = [ + "anyhow", "client", "collections", "component", + "db", + "futures 0.3.31", "gpui", "languages", "notifications", "project", + "serde", "ui", + "ui_input", + "util", "workspace", "workspace-hack", ] diff --git a/crates/component/src/component.rs b/crates/component/src/component.rs index 31ed169743..db847d5538 100644 --- a/crates/component/src/component.rs +++ b/crates/component/src/component.rs @@ -191,6 +191,14 @@ pub fn components() -> AllComponents { all_components } +// #[derive(Debug, Clone, PartialEq, Eq, Hash)] +// pub enum ComponentStatus { +// WorkInProgress, +// EngineeringReady, +// Live, +// Deprecated, +// } + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum ComponentScope { Collaboration, @@ -241,24 +249,30 @@ pub struct ComponentExample { impl RenderOnce for ComponentExample { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { div() + .pt_2() .w_full() .flex() .flex_col() .gap_3() .child( div() - .child(self.variant_name.clone()) - .text_size(rems(1.25)) - .text_color(cx.theme().colors().text), + .flex() + .flex_col() + .child( + div() + .child(self.variant_name.clone()) + .text_size(rems(1.0)) + .text_color(cx.theme().colors().text), + ) + .when_some(self.description, |this, description| { + this.child( + div() + .text_size(rems(0.875)) + .text_color(cx.theme().colors().text_muted) + .child(description.clone()), + ) + }), ) - .when_some(self.description, |this, description| { - this.child( - div() - .text_size(rems(0.9375)) - .text_color(cx.theme().colors().text_muted) - .child(description.clone()), - ) - }) .child( div() .flex() @@ -268,11 +282,11 @@ impl RenderOnce for ComponentExample { .justify_center() .p_8() .border_1() - .border_color(cx.theme().colors().border) + .border_color(cx.theme().colors().border.opacity(0.5)) .bg(pattern_slash( cx.theme().colors().surface_background.opacity(0.5), - 24.0, - 24.0, + 12.0, + 12.0, )) .shadow_sm() .child(self.element), diff --git a/crates/component_preview/Cargo.toml b/crates/component_preview/Cargo.toml index d8b4df34dd..e01e7d2208 100644 --- a/crates/component_preview/Cargo.toml +++ b/crates/component_preview/Cargo.toml @@ -16,12 +16,16 @@ default = [] [dependencies] client.workspace = true +collections.workspace = true component.workspace = true gpui.workspace = true languages.workspace = true +notifications.workspace = true project.workspace = true ui.workspace = true -workspace.workspace = true -notifications.workspace = true -collections.workspace = true +ui_input.workspace = true workspace-hack.workspace = true +workspace.workspace = true +db.workspace = true +anyhow.workspace = true +serde.workspace = true diff --git a/crates/component_preview/src/component_preview.rs b/crates/component_preview/src/component_preview.rs index 5109b8692d..276271828e 100644 --- a/crates/component_preview/src/component_preview.rs +++ b/crates/component_preview/src/component_preview.rs @@ -2,6 +2,8 @@ //! //! A view for exploring Zed components. +mod persistence; + use std::iter::Iterator; use std::sync::Arc; @@ -9,24 +11,27 @@ use client::UserStore; use component::{ComponentId, ComponentMetadata, components}; use gpui::{ App, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity, Window, list, prelude::*, - uniform_list, }; use collections::HashMap; -use gpui::{ListState, ScrollHandle, UniformListScrollHandle}; +use gpui::{ListState, ScrollHandle, ScrollStrategy, UniformListScrollHandle}; use languages::LanguageRegistry; use notifications::status_toast::{StatusToast, ToastIcon}; +use persistence::COMPONENT_PREVIEW_DB; use project::Project; -use ui::{Divider, ListItem, ListSubHeader, prelude::*}; +use ui::{Divider, HighlightedLabel, ListItem, ListSubHeader, prelude::*}; +use ui_input::SingleLineInput; use workspace::{AppState, ItemId, SerializableItem}; use workspace::{Item, Workspace, WorkspaceId, item::ItemEvent}; pub fn init(app_state: Arc, cx: &mut App) { + workspace::register_serializable_item::(cx); + let app_state = app_state.clone(); - cx.observe_new(move |workspace: &mut Workspace, _, cx| { + cx.observe_new(move |workspace: &mut Workspace, _window, cx| { let app_state = app_state.clone(); let weak_workspace = cx.entity().downgrade(); @@ -44,6 +49,7 @@ pub fn init(app_state: Arc, cx: &mut App) { user_store, None, None, + window, cx, ) }); @@ -64,13 +70,13 @@ pub fn init(app_state: Arc, cx: &mut App) { enum PreviewEntry { AllComponents, Separator, - Component(ComponentMetadata), + Component(ComponentMetadata, Option>), SectionHeader(SharedString), } impl From for PreviewEntry { fn from(component: ComponentMetadata) -> Self { - PreviewEntry::Component(component) + PreviewEntry::Component(component, None) } } @@ -88,6 +94,7 @@ enum PreviewPage { } struct ComponentPreview { + workspace_id: Option, focus_handle: FocusHandle, _view_scroll_handle: ScrollHandle, nav_scroll_handle: UniformListScrollHandle, @@ -99,6 +106,8 @@ struct ComponentPreview { language_registry: Arc, workspace: WeakEntity, user_store: Entity, + filter_editor: Entity, + filter_text: String, } impl ComponentPreview { @@ -108,11 +117,14 @@ impl ComponentPreview { user_store: Entity, selected_index: impl Into>, active_page: Option, + window: &mut Window, cx: &mut Context, ) -> Self { let sorted_components = components().all_sorted(); let selected_index = selected_index.into().unwrap_or(0); let active_page = active_page.unwrap_or(PreviewPage::AllComponents); + let filter_editor = + cx.new(|cx| SingleLineInput::new(window, cx, "Find components or usages…")); let component_list = ListState::new( sorted_components.len(), @@ -132,6 +144,7 @@ impl ComponentPreview { ); let mut component_preview = Self { + workspace_id: None, focus_handle: cx.focus_handle(), _view_scroll_handle: ScrollHandle::new(), nav_scroll_handle: UniformListScrollHandle::new(), @@ -143,6 +156,8 @@ impl ComponentPreview { components: sorted_components, component_list, cursor_index: selected_index, + filter_editor, + filter_text: String::new(), }; if component_preview.cursor_index > 0 { @@ -154,6 +169,13 @@ impl ComponentPreview { component_preview } + pub fn active_page_id(&self, _cx: &App) -> ActivePageId { + match &self.active_page { + PreviewPage::AllComponents => ActivePageId::default(), + PreviewPage::Component(component_id) => ActivePageId(component_id.0.to_string()), + } + } + fn scroll_to_preview(&mut self, ix: usize, cx: &mut Context) { self.component_list.scroll_to_reveal_item(ix); self.cursor_index = ix; @@ -162,6 +184,7 @@ impl ComponentPreview { fn set_active_page(&mut self, page: PreviewPage, cx: &mut Context) { self.active_page = page; + cx.emit(ItemEvent::UpdateTab); cx.notify(); } @@ -169,20 +192,94 @@ impl ComponentPreview { self.components[ix].clone() } + fn filtered_components(&self) -> Vec { + if self.filter_text.is_empty() { + return self.components.clone(); + } + + let filter = self.filter_text.to_lowercase(); + self.components + .iter() + .filter(|component| { + let component_name = component.name().to_lowercase(); + let scope_name = component.scope().to_string().to_lowercase(); + let description = component + .description() + .map(|d| d.to_lowercase()) + .unwrap_or_default(); + + component_name.contains(&filter) + || scope_name.contains(&filter) + || description.contains(&filter) + }) + .cloned() + .collect() + } + fn scope_ordered_entries(&self) -> Vec { use std::collections::HashMap; - let mut scope_groups: HashMap> = HashMap::default(); + let mut scope_groups: HashMap< + ComponentScope, + Vec<(ComponentMetadata, Option>)>, + > = HashMap::default(); + let lowercase_filter = self.filter_text.to_lowercase(); for component in &self.components { - scope_groups - .entry(component.scope()) - .or_insert_with(Vec::new) - .push(component.clone()); + if self.filter_text.is_empty() { + scope_groups + .entry(component.scope()) + .or_insert_with(Vec::new) + .push((component.clone(), None)); + continue; + } + + // let full_component_name = component.name(); + let scopeless_name = component.scopeless_name(); + let scope_name = component.scope().to_string(); + let description = component.description().unwrap_or_default(); + + let lowercase_scopeless = scopeless_name.to_lowercase(); + let lowercase_scope = scope_name.to_lowercase(); + let lowercase_desc = description.to_lowercase(); + + if lowercase_scopeless.contains(&lowercase_filter) { + if let Some(index) = lowercase_scopeless.find(&lowercase_filter) { + let end = index + lowercase_filter.len(); + + if end <= scopeless_name.len() { + let mut positions = Vec::new(); + for i in index..end { + if scopeless_name.is_char_boundary(i) { + positions.push(i); + } + } + + if !positions.is_empty() { + scope_groups + .entry(component.scope()) + .or_insert_with(Vec::new) + .push((component.clone(), Some(positions))); + continue; + } + } + } + } + + if lowercase_scopeless.contains(&lowercase_filter) + || lowercase_scope.contains(&lowercase_filter) + || lowercase_desc.contains(&lowercase_filter) + { + scope_groups + .entry(component.scope()) + .or_insert_with(Vec::new) + .push((component.clone(), None)); + } } + // Sort the components in each group for components in scope_groups.values_mut() { - components.sort_by_key(|c| c.name().to_lowercase()); + components.sort_by_key(|(c, _)| c.sort_name()); } let mut entries = Vec::new(); @@ -204,10 +301,10 @@ impl ComponentPreview { if !components.is_empty() { entries.push(PreviewEntry::SectionHeader(scope.to_string().into())); let mut sorted_components = components; - sorted_components.sort_by_key(|component| component.sort_name()); + sorted_components.sort_by_key(|(component, _)| component.sort_name()); - for component in sorted_components { - entries.push(PreviewEntry::Component(component)); + for (component, positions) in sorted_components { + entries.push(PreviewEntry::Component(component, positions)); } } } @@ -219,10 +316,10 @@ impl ComponentPreview { entries.push(PreviewEntry::Separator); entries.push(PreviewEntry::SectionHeader("Uncategorized".into())); let mut sorted_components = components.clone(); - sorted_components.sort_by_key(|c| c.sort_name()); + sorted_components.sort_by_key(|(c, _)| c.sort_name()); - for component in sorted_components { - entries.push(PreviewEntry::Component(component.clone())); + for (component, positions) in sorted_components { + entries.push(PreviewEntry::Component(component, positions)); } } } @@ -237,14 +334,33 @@ impl ComponentPreview { cx: &Context, ) -> impl IntoElement + use<> { match entry { - PreviewEntry::Component(component_metadata) => { + PreviewEntry::Component(component_metadata, highlight_positions) => { let id = component_metadata.id(); let selected = self.active_page == PreviewPage::Component(id.clone()); + let name = component_metadata.scopeless_name(); + ListItem::new(ix) - .child( - Label::new(component_metadata.scopeless_name().clone()) - .color(Color::Default), - ) + .child(if let Some(_positions) = highlight_positions { + let name_lower = name.to_lowercase(); + let filter_lower = self.filter_text.to_lowercase(); + let valid_positions = if let Some(start) = name_lower.find(&filter_lower) { + let end = start + filter_lower.len(); + (start..end).collect() + } else { + Vec::new() + }; + if valid_positions.is_empty() { + Label::new(name.clone()) + .color(Color::Default) + .into_any_element() + } else { + HighlightedLabel::new(name.clone(), valid_positions).into_any_element() + } + } else { + Label::new(name.clone()) + .color(Color::Default) + .into_any_element() + }) .selectable(true) .toggle_state(selected) .inset(true) @@ -282,20 +398,70 @@ impl ComponentPreview { } fn update_component_list(&mut self, cx: &mut Context) { - let new_len = self.scope_ordered_entries().len(); let entries = self.scope_ordered_entries(); + let new_len = entries.len(); let weak_entity = cx.entity().downgrade(); + if new_len > 0 { + self.nav_scroll_handle + .scroll_to_item(0, ScrollStrategy::Top); + } + + let filtered_components = self.filtered_components(); + + if !self.filter_text.is_empty() && !matches!(self.active_page, PreviewPage::AllComponents) { + if let PreviewPage::Component(ref component_id) = self.active_page { + let component_still_visible = filtered_components + .iter() + .any(|component| component.id() == *component_id); + + if !component_still_visible { + if !filtered_components.is_empty() { + let first_component = &filtered_components[0]; + self.set_active_page(PreviewPage::Component(first_component.id()), cx); + } else { + self.set_active_page(PreviewPage::AllComponents, cx); + } + } + } + } + + self.component_list = ListState::new( + filtered_components.len(), + gpui::ListAlignment::Top, + px(1500.0), + { + let components = filtered_components.clone(); + let this = cx.entity().downgrade(); + move |ix, window: &mut Window, cx: &mut App| { + if ix >= components.len() { + return div().w_full().h_0().into_any_element(); + } + + this.update(cx, |this, cx| { + let component = &components[ix]; + this.render_preview(component, window, cx) + .into_any_element() + }) + .unwrap() + } + }, + ); + let new_list = ListState::new( new_len, gpui::ListAlignment::Top, px(1500.0), move |ix, window, cx| { + if ix >= entries.len() { + return div().w_full().h_0().into_any_element(); + } + let entry = &entries[ix]; weak_entity .update(cx, |this, cx| match entry { - PreviewEntry::Component(component) => this + PreviewEntry::Component(component, _) => this .render_preview(component, window, cx) .into_any_element(), PreviewEntry::SectionHeader(shared_string) => this @@ -309,6 +475,7 @@ impl ComponentPreview { ); self.component_list = new_list; + cx.emit(ItemEvent::UpdateTab); } fn render_scope_header( @@ -377,16 +544,27 @@ impl ComponentPreview { .into_any_element() } - fn render_all_components(&self) -> impl IntoElement { + fn render_all_components(&self, cx: &Context) -> impl IntoElement { v_flex() .id("component-list") .px_8() .pt_4() .size_full() .child( - list(self.component_list.clone()) - .flex_grow() - .with_sizing_behavior(gpui::ListSizingBehavior::Auto), + if self.filtered_components().is_empty() && !self.filter_text.is_empty() { + div() + .size_full() + .items_center() + .justify_center() + .text_color(cx.theme().colors().text_muted) + .child(format!("No components matching '{}'.", self.filter_text)) + .into_any_element() + } else { + list(self.component_list.clone()) + .flex_grow() + .with_sizing_behavior(gpui::ListSizingBehavior::Auto) + .into_any_element() + }, ) } @@ -432,6 +610,19 @@ impl ComponentPreview { impl Render for ComponentPreview { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + // TODO: move this into the struct + let current_filter = self.filter_editor.update(cx, |input, cx| { + if input.is_empty(cx) { + String::new() + } else { + input.editor().read(cx).text(cx).to_string() + } + }); + + if current_filter != self.filter_text { + self.filter_text = current_filter; + self.update_component_list(cx); + } let sidebar_entries = self.scope_ordered_entries(); let active_page = self.active_page.clone(); @@ -449,14 +640,22 @@ impl Render for ComponentPreview { .border_color(cx.theme().colors().border) .h_full() .child( - uniform_list( + gpui::uniform_list( cx.entity().clone(), "component-nav", sidebar_entries.len(), move |this, range, _window, cx| { range - .map(|ix| { - this.render_sidebar_entry(ix, &sidebar_entries[ix], cx) + .filter_map(|ix| { + if ix < sidebar_entries.len() { + Some(this.render_sidebar_entry( + ix, + &sidebar_entries[ix], + cx, + )) + } else { + None + } }) .collect() }, @@ -481,12 +680,29 @@ impl Render for ComponentPreview { ), ), ) - .child(match active_page { - PreviewPage::AllComponents => self.render_all_components().into_any_element(), - PreviewPage::Component(id) => self - .render_component_page(&id, window, cx) - .into_any_element(), - }) + .child( + v_flex() + .id("content-area") + .flex_1() + .size_full() + .overflow_hidden() + .child( + div() + .p_2() + .w_full() + .border_b_1() + .border_color(cx.theme().colors().border) + .child(self.filter_editor.clone()), + ) + .child(match active_page { + PreviewPage::AllComponents => { + self.render_all_components(cx).into_any_element() + } + PreviewPage::Component(id) => self + .render_component_page(&id, window, cx) + .into_any_element(), + }), + ) } } @@ -498,6 +714,21 @@ impl Focusable for ComponentPreview { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ActivePageId(pub String); + +impl Default for ActivePageId { + fn default() -> Self { + ActivePageId("AllComponents".to_string()) + } +} + +impl From for ActivePageId { + fn from(id: ComponentId) -> Self { + ActivePageId(id.0.to_string()) + } +} + impl Item for ComponentPreview { type Event = ItemEvent; @@ -516,7 +747,7 @@ impl Item for ComponentPreview { fn clone_on_split( &self, _workspace_id: Option, - _window: &mut Window, + window: &mut Window, cx: &mut Context, ) -> Option> where @@ -535,6 +766,7 @@ impl Item for ComponentPreview { user_store, selected_index, Some(active_page), + window, cx, ) })) @@ -543,6 +775,15 @@ impl Item for ComponentPreview { fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) { f(*event) } + + fn added_to_workspace( + &mut self, + workspace: &mut Workspace, + _window: &mut Window, + _cx: &mut Context, + ) { + self.workspace_id = workspace.database_id(); + } } impl SerializableItem for ComponentPreview { @@ -553,26 +794,53 @@ impl SerializableItem for ComponentPreview { fn deserialize( project: Entity, workspace: WeakEntity, - _workspace_id: WorkspaceId, - _item_id: ItemId, + workspace_id: WorkspaceId, + item_id: ItemId, window: &mut Window, cx: &mut App, ) -> Task>> { + let deserialized_active_page = + match COMPONENT_PREVIEW_DB.get_active_page(item_id, workspace_id) { + Ok(page) => { + if let Some(page) = page { + ActivePageId(page) + } else { + ActivePageId::default() + } + } + Err(_) => ActivePageId::default(), + }; + let user_store = project.read(cx).user_store().clone(); let language_registry = project.read(cx).languages().clone(); + let preview_page = if deserialized_active_page.0 == ActivePageId::default().0 { + Some(PreviewPage::default()) + } else { + let component_str = deserialized_active_page.0; + let component_registry = components(); + let all_components = component_registry.all(); + let found_component = all_components.iter().find(|c| c.id().0 == component_str); + + if let Some(component) = found_component { + Some(PreviewPage::Component(component.id().clone())) + } else { + Some(PreviewPage::default()) + } + }; window.spawn(cx, async move |cx| { let user_store = user_store.clone(); let language_registry = language_registry.clone(); let weak_workspace = workspace.clone(); - cx.update(|_, cx| { + cx.update(move |window, cx| { Ok(cx.new(|cx| { ComponentPreview::new( weak_workspace, language_registry, user_store, None, - None, + preview_page, + window, cx, ) })) @@ -581,34 +849,41 @@ impl SerializableItem for ComponentPreview { } fn cleanup( - _workspace_id: WorkspaceId, - _alive_items: Vec, + workspace_id: WorkspaceId, + alive_items: Vec, _window: &mut Window, - _cx: &mut App, + cx: &mut App, ) -> Task> { - Task::ready(Ok(())) - // window.spawn(cx, |_| { - // ... - // }) + cx.background_spawn(async move { + COMPONENT_PREVIEW_DB + .delete_unloaded_items(workspace_id, alive_items) + .await + }) } fn serialize( &mut self, _workspace: &mut Workspace, - _item_id: ItemId, + item_id: ItemId, _closing: bool, _window: &mut Window, - _cx: &mut Context, + cx: &mut Context, ) -> Option>> { - // TODO: Serialize the active index so we can re-open to the same place - None + let active_page = self.active_page_id(cx); + let workspace_id = self.workspace_id?; + Some(cx.background_spawn(async move { + COMPONENT_PREVIEW_DB + .save_active_page(item_id, workspace_id, active_page.0) + .await + })) } - fn should_serialize(&self, _event: &Self::Event) -> bool { - false + fn should_serialize(&self, event: &Self::Event) -> bool { + matches!(event, ItemEvent::UpdateTab) } } +// TODO: use language registry to allow rendering markdown #[derive(IntoElement)] pub struct ComponentPreviewPage { // languages: Arc, diff --git a/crates/component_preview/src/persistence.rs b/crates/component_preview/src/persistence.rs new file mode 100644 index 0000000000..a3fb0c698b --- /dev/null +++ b/crates/component_preview/src/persistence.rs @@ -0,0 +1,73 @@ +use anyhow::Result; +use db::{define_connection, query, sqlez::statement::Statement, sqlez_macros::sql}; +use workspace::{ItemId, WorkspaceDb, WorkspaceId}; + +define_connection! { + pub static ref COMPONENT_PREVIEW_DB: ComponentPreviewDb = + &[sql!( + CREATE TABLE component_previews ( + workspace_id INTEGER, + item_id INTEGER UNIQUE, + active_page_id TEXT, + PRIMARY KEY(workspace_id, item_id), + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ) STRICT; + )]; +} + +impl ComponentPreviewDb { + pub async fn save_active_page( + &self, + item_id: ItemId, + workspace_id: WorkspaceId, + active_page_id: String, + ) -> Result<()> { + let query = "INSERT INTO component_previews(item_id, workspace_id, active_page_id) + VALUES (?1, ?2, ?3) + ON CONFLICT DO UPDATE SET + active_page_id = ?3"; + self.write(move |conn| { + let mut statement = Statement::prepare(conn, query)?; + let mut next_index = statement.bind(&item_id, 1)?; + next_index = statement.bind(&workspace_id, next_index)?; + statement.bind(&active_page_id, next_index)?; + statement.exec() + }) + .await + } + + query! { + pub fn get_active_page(item_id: ItemId, workspace_id: WorkspaceId) -> Result> { + SELECT active_page_id + FROM component_previews + WHERE item_id = ? AND workspace_id = ? + } + } + + pub async fn delete_unloaded_items( + &self, + workspace: WorkspaceId, + alive_items: Vec, + ) -> Result<()> { + let placeholders = alive_items + .iter() + .map(|_| "?") + .collect::>() + .join(", "); + + let query = format!( + "DELETE FROM component_previews WHERE workspace_id = ? AND item_id NOT IN ({placeholders})" + ); + + self.write(move |conn| { + let mut statement = Statement::prepare(conn, query)?; + let mut next_index = statement.bind(&workspace, 1)?; + for id in alive_items { + next_index = statement.bind(&id, next_index)?; + } + statement.exec() + }) + .await + } +} diff --git a/crates/ui/src/components/avatar.rs b/crates/ui/src/components/avatar.rs index 668bdd5285..3ab31acc1b 100644 --- a/crates/ui/src/components/avatar.rs +++ b/crates/ui/src/components/avatar.rs @@ -236,53 +236,30 @@ impl Component for Avatar { v_flex() .gap_6() .children(vec![ + example_group(vec![ + single_example("Default", Avatar::new(example_avatar).into_any_element()), + single_example( + "Grayscale", + Avatar::new(example_avatar) + .grayscale(true) + .into_any_element(), + ), + single_example( + "Border", + Avatar::new(example_avatar) + .border_color(cx.theme().colors().border) + .into_any_element(), + ).description("Can be used to create visual space by setting the border color to match the background, which creates the appearance of a gap around the avatar."), + ]), example_group_with_title( - "Sizes", - vec![ - single_example( - "Default", - Avatar::new(example_avatar).into_any_element(), - ), - single_example( - "Small", - Avatar::new(example_avatar).size(px(24.)).into_any_element(), - ), - single_example( - "Large", - Avatar::new(example_avatar).size(px(48.)).into_any_element(), - ), - ], - ), - example_group_with_title( - "Styles", - vec![ - single_example( - "Default", - Avatar::new(example_avatar).into_any_element(), - ), - single_example( - "Grayscale", - Avatar::new(example_avatar) - .grayscale(true) - .into_any_element(), - ), - single_example( - "With Border", - Avatar::new(example_avatar) - .border_color(cx.theme().colors().border) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Audio Status", + "Indicator Styles", vec![ single_example( "Muted", Avatar::new(example_avatar) .indicator(AvatarAudioStatusIndicator::new(AudioStatus::Muted)) .into_any_element(), - ), + ).description("Indicates the collaborator's mic is muted."), single_example( "Deafened", Avatar::new(example_avatar) @@ -290,28 +267,23 @@ impl Component for Avatar { AudioStatus::Deafened, )) .into_any_element(), - ), - ], - ), - example_group_with_title( - "Availability", - vec![ + ).description("Indicates that both the collaborator's mic and audio are muted."), single_example( - "Free", + "Availability: Free", Avatar::new(example_avatar) .indicator(AvatarAvailabilityIndicator::new( CollaboratorAvailability::Free, )) .into_any_element(), - ), + ).description("Indicates that the person is free, usually meaning they are not in a call."), single_example( - "Busy", + "Availability: Busy", Avatar::new(example_avatar) .indicator(AvatarAvailabilityIndicator::new( CollaboratorAvailability::Busy, )) .into_any_element(), - ), + ).description("Indicates that the person is busy, usually meaning they are in a channel or direct call."), ], ), ]) diff --git a/crates/ui/src/components/keybinding.rs b/crates/ui/src/components/keybinding.rs index db9bb3008f..1b3746515d 100644 --- a/crates/ui/src/components/keybinding.rs +++ b/crates/ui/src/components/keybinding.rs @@ -451,7 +451,7 @@ fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode impl Component for KeyBinding { fn scope() -> ComponentScope { - ComponentScope::Input + ComponentScope::Typography } fn name() -> &'static str {