diff --git a/component.json b/component.json index c84d50e3..4e8cbadc 100644 --- a/component.json +++ b/component.json @@ -37,6 +37,7 @@ "preview/src/components/textarea", "preview/src/components/skeleton", "preview/src/components/card", - "preview/src/components/sheet" + "preview/src/components/sheet", + "preview/src/components/badge" ] } diff --git a/playwright/avatar.spec.ts b/playwright/avatar.spec.ts index 4f6238f1..77ad6bcc 100644 --- a/playwright/avatar.spec.ts +++ b/playwright/avatar.spec.ts @@ -8,8 +8,8 @@ test("test", async ({ page }) => { let image = avatar.locator("img"); await expect(image).toHaveAttribute("src", "https://avatars.githubusercontent.com/u/66571940?s=96&v=4"); - // Get the second avatar element - const secondAvatar = page.locator(".avatar-item").nth(1); - // Verify the second avatar has fallback text - await expect(secondAvatar).toContainText("JK"); + // Get the third avatar element (Error State - has invalid image URL, shows fallback) + const errorAvatar = page.locator(".avatar-item").nth(2); + // Verify the error state avatar has fallback text + await expect(errorAvatar).toContainText("JK"); }); diff --git a/preview/src/components/avatar/component.rs b/preview/src/components/avatar/component.rs index ddb43d77..8cf019ba 100644 --- a/preview/src/components/avatar/component.rs +++ b/preview/src/components/avatar/component.rs @@ -19,6 +19,22 @@ impl AvatarImageSize { } } +#[derive(Clone, Copy, PartialEq, Default)] +pub enum AvatarShape { + #[default] + Circle, + Rounded, +} + +impl AvatarShape { + fn to_class(self) -> &'static str { + match self { + AvatarShape::Circle => "avatar-circle", + AvatarShape::Rounded => "avatar-rounded", + } + } +} + /// The props for the [`Avatar`] component. #[derive(Props, Clone, PartialEq)] pub struct AvatarProps { @@ -37,6 +53,9 @@ pub struct AvatarProps { #[props(default)] pub size: AvatarImageSize, + #[props(default)] + pub shape: AvatarShape, + /// Additional attributes for the avatar element #[props(extends = GlobalAttributes)] pub attributes: Vec, @@ -51,7 +70,7 @@ pub fn Avatar(props: AvatarProps) -> Element { document::Link { rel: "stylesheet", href: asset!("./style.css") } avatar::Avatar { - class: "avatar {props.size.to_class()}", + class: "avatar {props.size.to_class()} {props.shape.to_class()}", on_load: props.on_load, on_error: props.on_error, on_state_change: props.on_state_change, diff --git a/preview/src/components/avatar/style.css b/preview/src/components/avatar/style.css index 2d487dc1..d239e02e 100644 --- a/preview/src/components/avatar/style.css +++ b/preview/src/components/avatar/style.css @@ -21,7 +21,6 @@ flex-shrink: 0; align-items: center; justify-content: center; - border-radius: 3.40282e+38px; color: var(--secondary-color-4); cursor: pointer; font-weight: 500; @@ -52,13 +51,22 @@ font-size: 1.75rem; } +/* Avatar shape */ +.avatar-circle { + border-radius: 50%; +} + +.avatar-rounded { + border-radius: 8px; +} + /* State-specific styles */ .avatar[data-state="loading"] { animation: pulse 1.5s infinite ease-in-out; } .avatar[data-state="empty"] { - background: var(--primary-color-2); + background: var(--primary-color-7); } @keyframes pulse { diff --git a/preview/src/components/avatar/variants/main/mod.rs b/preview/src/components/avatar/variants/main/mod.rs index 3674b993..7c69da77 100644 --- a/preview/src/components/avatar/variants/main/mod.rs +++ b/preview/src/components/avatar/variants/main/mod.rs @@ -27,12 +27,29 @@ pub fn Demo() -> Element { AvatarFallback { class: "avatar-fallback", "EA" } } } + div { class: "avatar-item", + p { class: "avatar-label", "Rounded" } + Avatar { + size: AvatarImageSize::Small, + shape: AvatarShape::Rounded, + on_state_change: move |state| { + avatar_state.set(format!("Avatar 2: {state:?}")); + }, + aria_label: "Basic avatar", + AvatarImage { + class: "avatar-image", + src: "https://avatars.githubusercontent.com/u/66571940?s=96&v=4", + alt: "User avatar", + } + AvatarFallback { class: "avatar-fallback", "EA" } + } + } div { class: "avatar-item", p { class: "avatar-label", "Error State" } Avatar { size: AvatarImageSize::Medium, on_state_change: move |state| { - avatar_state.set(format!("Avatar 2: {state:?}")); + avatar_state.set(format!("Avatar 3: {state:?}")); }, aria_label: "Error avatar", AvatarImage { diff --git a/preview/src/components/badge/component.json b/preview/src/components/badge/component.json new file mode 100644 index 00000000..792f5c6a --- /dev/null +++ b/preview/src/components/badge/component.json @@ -0,0 +1,13 @@ +{ + "name": "badge", + "description": "A small label to display status or categorization", + "authors": ["Evan Almloff"], + "exclude": ["variants", "docs.md", "component.json"], + "cargoDependencies": [ + { + "name": "dioxus-primitives", + "git": "https://github.com/DioxusLabs/components" + } + ], + "globalAssets": ["../../../assets/dx-components-theme.css"] +} diff --git a/preview/src/components/badge/component.rs b/preview/src/components/badge/component.rs new file mode 100644 index 00000000..cd078aac --- /dev/null +++ b/preview/src/components/badge/component.rs @@ -0,0 +1,81 @@ +use dioxus::prelude::*; + +#[derive(Copy, Clone, PartialEq, Default)] +#[non_exhaustive] +pub enum BadgeVariant { + #[default] + Primary, + Secondary, + Destructive, + Outline, +} + +impl BadgeVariant { + pub fn class(&self) -> &'static str { + match self { + BadgeVariant::Primary => "primary", + BadgeVariant::Secondary => "secondary", + BadgeVariant::Destructive => "destructive", + BadgeVariant::Outline => "outline", + } + } +} + +/// The props for the [`Badge`] component. +#[derive(Props, Clone, PartialEq)] +pub struct BadgeProps { + #[props(default)] + pub variant: BadgeVariant, + + /// Additional attributes to extend the badge element + #[props(extends = GlobalAttributes)] + pub attributes: Vec, + + /// The children of the badge element + pub children: Element, +} + +#[component] +pub fn Badge(props: BadgeProps) -> Element { + rsx! { + document::Link { rel: "stylesheet", href: asset!("./style.css") } + + BadgeElement { + "padding": true, + variant: props.variant, + attributes: props.attributes, + {props.children} + } + } +} + +#[component] +fn BadgeElement(props: BadgeProps) -> Element { + rsx! { + span { + class: "badge", + "data-style": props.variant.class(), + ..props.attributes, + {props.children} + } + } +} + +#[component] +pub fn VerifiedIcon() -> Element { + rsx! { + svg { + view_box: "0 0 24 24", + xmlns: "http://www.w3.org/2000/svg", + width: "12", + height: "12", + fill: "none", + stroke: "var(--secondary-color-4)", + stroke_linecap: "round", + stroke_linejoin: "round", + stroke_width: 2, + path { d: "M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z" } + path { d: "m9 12 2 2 4-4" } + } + } +} diff --git a/preview/src/components/badge/docs.md b/preview/src/components/badge/docs.md new file mode 100644 index 00000000..ba1638ef --- /dev/null +++ b/preview/src/components/badge/docs.md @@ -0,0 +1,9 @@ +The Badge is a component designed to display small, distinct labels that help highlight important content or status indicators. Perfect for use cases like notifications, status labels, or categorization. + +## Component Structure + +```rust +Badge { + {children} +} +``` \ No newline at end of file diff --git a/preview/src/components/badge/mod.rs b/preview/src/components/badge/mod.rs new file mode 100644 index 00000000..9a8ae556 --- /dev/null +++ b/preview/src/components/badge/mod.rs @@ -0,0 +1,2 @@ +mod component; +pub use component::*; \ No newline at end of file diff --git a/preview/src/components/badge/style.css b/preview/src/components/badge/style.css new file mode 100644 index 00000000..e36df538 --- /dev/null +++ b/preview/src/components/badge/style.css @@ -0,0 +1,42 @@ +.badge-example { + display: flex; + align-items: center; + gap: 1rem; +} + +.badge { + display: inline-flex; + min-width: 20px; + height: 20px; + align-items: center; + justify-content: center; + border-radius: 10px; + box-shadow: 0 0 0 1px var(--primary-color-2); + font-size: 12px; + gap: 4px +} + +.badge[padding="true"] { + padding: 0 8px; +} + +.badge[data-style="primary"] { + background-color: var(--secondary-color-2); + color: var(--primary-color); +} + +.badge[data-style="secondary"] { + background-color: var(--primary-color-5); + color: var(--secondary-color-1); +} + +.badge[data-style="outline"] { + border: 1px solid var(--primary-color-6); + background-color: var(--light, var(--primary-color)) var(--dark, var(--primary-color-3)); + color: var(--secondary-color-4); +} + +.badge[data-style="destructive"] { + background-color: var(--primary-error-color); + color: var(--contrast-error-color); +} \ No newline at end of file diff --git a/preview/src/components/badge/variants/main/mod.rs b/preview/src/components/badge/variants/main/mod.rs new file mode 100644 index 00000000..cf5deb9e --- /dev/null +++ b/preview/src/components/badge/variants/main/mod.rs @@ -0,0 +1,22 @@ +use dioxus::prelude::*; + +use super::super::component::*; + +#[component] +pub fn Demo() -> Element { + rsx! { + div { class: "badge-example", + + Badge { "Primary" } + Badge { variant: BadgeVariant::Secondary, "Secondary" } + Badge { variant: BadgeVariant::Destructive, "Destructive" } + Badge { variant: BadgeVariant::Outline, "Outline" } + Badge { + variant: BadgeVariant::Secondary, + style: "background-color: var(--focused-border-color)", + VerifiedIcon {} + "Verified" + } + } + } +} diff --git a/preview/src/components/mod.rs b/preview/src/components/mod.rs index 82b3e082..747df071 100644 --- a/preview/src/components/mod.rs +++ b/preview/src/components/mod.rs @@ -61,6 +61,7 @@ examples!( alert_dialog, aspect_ratio, avatar, + badge, button, calendar[simple, internationalized, range, multi_month, unavailable_dates], checkbox, diff --git a/primitives/src/date_picker.rs b/primitives/src/date_picker.rs index f9355c82..b1b0ec78 100644 --- a/primitives/src/date_picker.rs +++ b/primitives/src/date_picker.rs @@ -103,7 +103,7 @@ pub struct DatePickerProps { /// use dioxus_primitives::{calendar::Calendar, date_picker::*, popover::*, ContentAlign}; /// use time::Date; /// #[component] -/// pub fn Demo() -> Element { +/// fn Demo() -> Element { /// let mut selected_date = use_signal(|| None::); /// rsx! { /// div { @@ -242,7 +242,7 @@ pub struct DateRangePickerProps { /// use dioxus::prelude::*; /// use dioxus_primitives::{calendar::{DateRange, RangeCalendar}, date_picker::*, popover::*, ContentAlign}; /// #[component] -/// pub fn Demo() -> Element { +/// fn Demo() -> Element { /// let mut selected_range = use_signal(|| None::); /// rsx! { /// div { @@ -350,7 +350,7 @@ pub struct DatePickerPopoverProps { /// use dioxus_primitives::{calendar::Calendar, date_picker::*, popover::*, ContentAlign}; /// use time::Date; /// #[component] -/// pub fn Demo() -> Element { +/// fn Demo() -> Element { /// let mut selected_date = use_signal(|| None::); /// rsx! { /// div { @@ -473,7 +473,7 @@ pub struct DatePickerCalendarProps Element { +/// fn Demo() -> Element { /// let mut selected_date = use_signal(|| None::); /// rsx! { /// div { @@ -550,7 +550,7 @@ pub fn DatePickerCalendar(props: DatePickerCalendarProps) -> Elem /// use dioxus::prelude::*; /// use dioxus_primitives::{calendar::{DateRange, RangeCalendar}, date_picker::*, popover::*, ContentAlign}; /// #[component] -/// pub fn Demo() -> Element { +/// fn Demo() -> Element { /// let mut selected_range = use_signal(|| None::); /// rsx! { /// div { @@ -1010,7 +1010,7 @@ pub struct DatePickerInputProps { /// use dioxus_primitives::{calendar::Calendar, date_picker::*, popover::*, ContentAlign}; /// use time::Date; /// #[component] -/// pub fn Demo() -> Element { +/// fn Demo() -> Element { /// let mut selected_date = use_signal(|| None::); /// rsx! { /// div { @@ -1069,7 +1069,7 @@ pub fn DatePickerInput(props: DatePickerInputProps) -> Element { /// use dioxus::prelude::*; /// use dioxus_primitives::{calendar::{DateRange, RangeCalendar}, date_picker::*, popover::*, ContentAlign}; /// #[component] -/// pub fn Demo() -> Element { +/// fn Demo() -> Element { /// let mut selected_range = use_signal(|| None::); /// rsx! { /// div {