From e55b46905c9be675ef6dddbed0807496aa3fd970 Mon Sep 17 00:00:00 2001 From: luoxiao Date: Sun, 2 Mar 2025 16:34:27 +0800 Subject: [PATCH] feat: Adds Persona component --- demo/src/app.rs | 1 + demo/src/pages/components.rs | 5 ++ demo_markdown/src/lib.rs | 1 + thaw/src/lib.rs | 2 + thaw/src/persona/docs/mod.md | 168 +++++++++++++++++++++++++++++++++++ thaw/src/persona/mod.rs | 139 +++++++++++++++++++++++++++++ thaw/src/persona/persona.css | 96 ++++++++++++++++++++ thaw/src/persona/types.rs | 90 +++++++++++++++++++ 8 files changed, 502 insertions(+) create mode 100644 thaw/src/persona/docs/mod.md create mode 100644 thaw/src/persona/mod.rs create mode 100644 thaw/src/persona/persona.css create mode 100644 thaw/src/persona/types.rs diff --git a/demo/src/app.rs b/demo/src/app.rs index 1eafbf18..cd543a4f 100644 --- a/demo/src/app.rs +++ b/demo/src/app.rs @@ -95,6 +95,7 @@ fn TheRouter() -> impl IntoView { + diff --git a/demo/src/pages/components.rs b/demo/src/pages/components.rs index 3efd8bb8..026b0406 100644 --- a/demo/src/pages/components.rs +++ b/demo/src/pages/components.rs @@ -344,6 +344,11 @@ pub(crate) fn gen_nav_data() -> Vec { value: "/components/pagination", label: "Pagination", }, + NavItemOption { + group: None, + value: "/components/persona", + label: "Persona", + }, NavItemOption { group: None, value: "/components/popover", diff --git a/demo_markdown/src/lib.rs b/demo_markdown/src/lib.rs index f3d682c3..ce4dfe3d 100644 --- a/demo_markdown/src/lib.rs +++ b/demo_markdown/src/lib.rs @@ -59,6 +59,7 @@ pub fn include_md(_token_stream: proc_macro::TokenStream) -> proc_macro::TokenSt "MessageBarMdPage" => "../../thaw/src/message_bar/docs/mod.md", "NavMdPage" => "../../thaw/src/nav/docs/mod.md", "PaginationMdPage" => "../../thaw/src/pagination/docs/mod.md", + "PersonaMdPage" => "../../thaw/src/persona/docs/mod.md", "PopoverMdPage" => "../../thaw/src/popover/docs/mod.md", "ProgressBarMdPage" => "../../thaw/src/progress_bar/docs/mod.md", "RadioMdPage" => "../../thaw/src/radio/docs/mod.md", diff --git a/thaw/src/lib.rs b/thaw/src/lib.rs index 598b50b1..83b98ba7 100644 --- a/thaw/src/lib.rs +++ b/thaw/src/lib.rs @@ -34,6 +34,7 @@ mod menu; mod message_bar; mod nav; mod pagination; +mod persona; mod popover; mod progress_bar; mod radio; @@ -92,6 +93,7 @@ pub use menu::*; pub use message_bar::*; pub use nav::*; pub use pagination::*; +pub use persona::*; pub use popover::*; pub use progress_bar::*; pub use radio::*; diff --git a/thaw/src/persona/docs/mod.md b/thaw/src/persona/docs/mod.md new file mode 100644 index 00000000..0fdf8f14 --- /dev/null +++ b/thaw/src/persona/docs/mod.md @@ -0,0 +1,168 @@ +# Persona + +```rust demo +view! { + + + "UI" + + +} +``` + +### Text Alignment + +A Persona supports two text alignments, `start` being the default position. + +```rust demo +view! { + + + + "UI" + + + "Card" + + + "Header" + + + + + "UI" + + + "Card" + + + "Header" + + + +} +``` + +### Text Position + +A Persona supports three text positions, `after` being the default position. + +```rust demo +view! { + + + + "UI" + + + + + "UI" + + + + + "UI" + + + +} +``` + +### Avatar Size + +A Persona supports different sizes, medium being the default. + +```rust demo +view! { + + + + "UI" + + + + + "UI" + + + + + "UI" + + + + + "UI" + + + + + "UI" + + + + + "UI" + + + +} +``` + +### Persona Props + +| Name | Type | Default | Description | +| --- | --- | --- | --- | +| class | `MaybeProp` | `Default::default()` | | +| style | `MaybeProp` | `Default::default()` | | +| name | `MaybeProp` | `None` | The name of the person or entity represented by the Persona. | +| size | `Signal` | `PersonaSize::Medium` | The size of a Persona and its text. | +| text_alignment | `Signal` | `PersonaTextAlignment::Start` | The vertical alignment of the text relative to the avatar. | +| text_position | `Signal` | `PersonaTextPosition::After` | The position of the text relative to the avatar. | +| persona_primary_text | slot `Option` | `None` | The first line of text in the Persona, larger than the rest of the lines. | +| persona_secondary_text | slot `Option` | `None` | The second line of text in the Persona. | +| persona_tertiary_text | slot `Option` | `None` | The third line of text in the Persona. | +| persona_quaternary_text | slot `Option` | `None` | The fourth line of text in the Persona. | + +### PersonaPrimaryText & PersonaSecondaryText & PersonaTertiaryText & PersonaQuaternaryText Props + +| Name | Type | Default | Description | +| -------- | ---------- | ------- | ----------- | +| children | `Children` | | | diff --git a/thaw/src/persona/mod.rs b/thaw/src/persona/mod.rs new file mode 100644 index 00000000..a033c209 --- /dev/null +++ b/thaw/src/persona/mod.rs @@ -0,0 +1,139 @@ +mod types; + +pub use types::*; + +use crate::Avatar; +use leptos::{either::Either, prelude::*}; +use thaw_components::{If, Then}; +use thaw_utils::{class_list, mount_style}; + +#[component] +pub fn Persona( + #[prop(optional, into)] class: MaybeProp, + #[prop(optional, into)] style: MaybeProp, + /// The name of the person or entity represented by the Persona. + /// When primary_text is not provided, this will be used as the default value for primaryText. + #[prop(optional, into)] + name: MaybeProp, + /// The size of a Persona and its text. + #[prop(optional, into)] + size: Signal, + /// The vertical alignment of the text relative to the avatar. + #[prop(optional, into)] + text_alignment: Signal, + /// The position of the text relative to the avatar. + #[prop(optional, into)] + text_position: Signal, + #[prop(optional, into)] avatar_src: MaybeProp, + /// The first line of text in the Persona, larger than the rest of the lines. + #[prop(optional)] + persona_primary_text: Option, + /// The second line of text in the Persona. + #[prop(optional)] + persona_secondary_text: Option, + /// The third line of text in the Persona. + #[prop(optional)] + persona_tertiary_text: Option, + /// The fourth line of text in the Persona. + #[prop(optional)] + persona_quaternary_text: Option, +) -> impl IntoView { + mount_style("persona", include_str!("./persona.css")); + + let text_position_before = + Memo::new(move |_| text_position.get() == PersonaTextPosition::Before); + + let style = move || { + let css_var = match size.get() { + PersonaSize::ExtraSmall => "spacingHorizontalSNudge", + PersonaSize::Small => "spacingHorizontalS", + PersonaSize::Medium => "spacingHorizontalS", + PersonaSize::Large => "spacingHorizontalMNudge", + PersonaSize::ExtraLarge => "spacingHorizontalMNudge", + PersonaSize::Huge => "spacingHorizontalM", + }; + + let mut s = format!("--thaw-persona__avatar-spacing: var(--{css_var});"); + + style.with(|style| { + if let Some(style) = style.as_ref() { + s.push_str(style); + } + }); + + s + }; + + let avatar_size = Memo::new(move |_| size.get().as_avatar_size()); + + view! { +
+ + + + + + {if let Some(text) = persona_primary_text { + Either::Left( + view! { {(text.children)()} }, + ) + } else { + Either::Right(move || { + if let Some(name) = name.get() { + Either::Left( + view! { {name} }, + ) + } else { + Either::Right(()) + } + }) + }} + {if let Some(text) = persona_secondary_text { + Either::Left( + view! { {(text.children)()} }, + ) + } else { + Either::Right(()) + }} + {if let Some(text) = persona_tertiary_text { + Either::Left( + view! { {(text.children)()} }, + ) + } else { + Either::Right(()) + }} + {if let Some(text) = persona_quaternary_text { + Either::Left( + view! { {(text.children)()} }, + ) + } else { + Either::Right(()) + }} + + + + + + +
+ } +} diff --git a/thaw/src/persona/persona.css b/thaw/src/persona/persona.css new file mode 100644 index 00000000..919df764 --- /dev/null +++ b/thaw/src/persona/persona.css @@ -0,0 +1,96 @@ +.thaw-persona { + display: inline-grid; + grid-auto-rows: max-content; + grid-auto-flow: column; + justify-items: start; + grid-template-columns: max-content [middle] auto; +} + +.thaw-persona.thaw-persona--center:not(.thaw-persona--below) { + /* This template is needed to make sure the Avatar is centered when it takes up more space than the text lines */ + grid-template-rows: 1fr [primary] max-content [secondary] max-content [tertiary] max-content [quaternary] max-content 1fr; +} + +.thaw-persona--before { + justify-items: end; + grid-template-columns: auto [middle] max-content; +} + +.thaw-persona--below { + grid-auto-flow: unset; + justify-items: center; + grid-template-columns: unset; +} + +.thaw-persona:not(.thaw-persona--below) .thaw-persona__avatar { + grid-row-start: span 5; +} + +.thaw-persona--start .thaw-persona__avatar { + align-self: start; +} + +.thaw-persona--center .thaw-persona__avatar { + align-self: center; +} + +.thaw-persona.thaw-persona--center:not(.thaw-persona--below) + .thaw-persona__avatar { + grid-row-start: span 6; +} + +.thaw-persona--after .thaw-persona__avatar { + margin-right: var(--thaw-persona__avatar-spacing); +} + +.thaw-persona--below .thaw-persona__avatar { + margin-bottom: var(--thaw-persona__avatar-spacing); +} + +.thaw-persona--before .thaw-persona__avatar { + margin-left: var(--thaw-persona__avatar-spacing); +} + +.thaw-persona__primary-text { + display: block; + color: var(--colorNeutralForeground1); + font-family: var(--fontFamilyBase); + font-size: var(--fontSizeBase300); + font-weight: var(--fontWeightRegular); + line-height: var(--lineHeightBase300); +} + +.thaw-persona__secondary-text { + margin-top: -2px; +} + +.thaw-persona__secondary-text, +.thaw-persona__tertiary-text, +.thaw-persona__quaternary-text { + display: block; + color: var(--colorNeutralForeground2); + font-family: var(--fontFamilyBase); + font-size: var(--fontSizeBase200); + font-weight: var(--fontWeightRegular); + line-height: var(--lineHeightBase200); +} + +.thaw-persona.thaw-persona--center:not(.thaw-persona--below) + .thaw-persona__primary-text { + grid-row-start: primary; +} + +.thaw-persona.thaw-persona--center:not(.thaw-persona--below) + .thaw-persona__secondary-text { + grid-row-start: secondary; +} + +.thaw-persona.thaw-persona--center:not(.thaw-persona--below) + .thaw-persona__tertiary-text { + grid-row-start: tertiary; +} + +.thaw-persona.thaw-persona--center:not(.thaw-persona--below) + .thaw-persona__quaternary-text { + grid-row-start: quaternary; +} diff --git a/thaw/src/persona/types.rs b/thaw/src/persona/types.rs new file mode 100644 index 00000000..ccc84671 --- /dev/null +++ b/thaw/src/persona/types.rs @@ -0,0 +1,90 @@ +use leptos::prelude::*; + +#[slot] +pub struct PersonaPrimaryText { + children: Children, +} + +#[slot] +pub struct PersonaSecondaryText { + children: Children, +} + +#[slot] +pub struct PersonaTertiaryText { + children: Children, +} + +#[slot] +pub struct PersonaQuaternaryText { + children: Children, +} + +#[derive(Debug, Default, Clone, PartialEq)] +pub enum PersonaTextAlignment { + #[default] + Start, + Center, +} + +impl PersonaTextAlignment { + pub fn as_str(&self) -> &'static str { + match self { + Self::Start => "start", + Self::Center => "center", + } + } +} + +#[derive(Debug, Default, Clone, PartialEq)] +pub enum PersonaTextPosition { + Before, + #[default] + After, + Below, +} + +impl PersonaTextPosition { + pub fn as_str(&self) -> &'static str { + match self { + Self::Before => "before", + Self::After => "after", + Self::Below => "below", + } + } +} + +#[derive(Debug, Default, Clone, PartialEq)] +pub enum PersonaSize { + ExtraSmall, + Small, + #[default] + Medium, + Large, + ExtraLarge, + Huge, +} + +impl PersonaSize { + pub(crate) fn as_avatar_size(&self) -> u8 { + match self { + Self::ExtraSmall => 20, + Self::Small => 28, + Self::Medium => 32, + Self::Large => 36, + Self::ExtraLarge => 40, + Self::Huge => 56, + } + } + + pub fn as_str(&self) -> &'static str { + match self { + Self::ExtraSmall => "extra-small", + Self::Small => "small", + Self::Medium => "medium", + Self::Large => "large", + Self::ExtraLarge => "extra-large", + Self::Huge => "huge", + } + } +}